概述
自认为学过c++的我,看过《c++ premier》的第十二章“类”后,不禁感到汗颜,很多关于类的特性我几乎都一无所知,还常常跟java混为一谈。折腾了一天,修改了多次,才算最终把一些问题搞清楚了。呵呵,现在我还不好意思说自己搞清楚了,谁知道还有多少我不知道的呢!所以真的是要stay foolish, stay hungry啊,很多问题并不是我们以为的那么显而易见的。
1. 类的声明与定义
前面我们区分过变量的声明与定义。定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。声明用于向程序表明变量的类型和名字(所以定义也是一个声明),可以通过使用extern关键字声明变量而不定义它。
类的声明与定义跟变量的声明与定义的区别在某些程度上是相似的。类的定义给出了类包含的数据成员、成员函数,这样,一旦我们定义了类,我们就知道了所有的类成员,以及存储该类型的对象所需要的存储空间。在一个给定的源文件中,一个类只能被定义一次(就如同一个变量有且仅有一个定义)。
但是,我们也可以声明一个类而不定义它:
class Sales_item;
这个声明,有时称为向前声明(forward declaration),在程序中引入了类类型的Sales_item。在声明之后、定义之前,类Sales_item是一个不完全类型(incomplete type),即已知Sales_item是一个类型,但不知道它包含哪些成员。
我们可以在类定义中使用类型别名来简化类,如下示例:
class Screen{
public:
typedef std::string::size_type index;
private:
std::string contents;
index cursor;
index height, weight;
}
这样,index就成为类Screen定义的一种类型,在外面使用的时候可以用Screen::index来表示。
可以有三种方式声明内联函数(inline):
一种是在类的定义体中,声明函数并给出函数的定义;
一种是在类的定义体中,将函数声明成inline的;
第三种是在类的外部定义成员函数时,再将其声明成inline的。
class Screen{
public:
typedef std::string::size_type index;
//default inline function, type 1.
char get() const { return contents[cursor]; }
//declare as inline function in class definition block, type2
inline char get( index ht, index wd ) const;
index getCursor() const;
private:
std::string contents;
index cursor;
index height, width;
}
//already declared as inline function, no need to repeat
char Screen::get( index ht, index wd ) const{
index row = ht*width;
return contents[row+wd];
}
//defined as inline function, type 3
inline Screen::index getCursor() const{
return cursor;
}
这段代码还同时涉及其他两个问题,我们且把它命名为“代码片段2”,方便后面回指。
2. 隐含的this指针
相信this指针谁都不陌生,前面的文章也提到过const成员函数(代码片段2中的三个成员函数均为const成员函数)中,函数后面跟着的const的意义,就是指的不修改this的数据成员。
有时候我们会把整个对象返回回来以使某些操作的序列能连接成一个单独的表达式,这时候我们会返回*this,在普通的非const成员函数中,this的类型是一个指向类类型的const指针,即可以改变const所指向的值,但不能改变this中所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针,即不能改变this所指向的值,也不能改变this所保存的地址。
*基于const的重载(这一点我以前确实是什么没有想过,两个函数,一个是const成员函数,返回const的对象引用,一个是非const成员函数,返回普通的对象引用,形参是一样的。)
class Screen{
public:
Screen& display( std::ostream &os ){
do_display(os); return *this;
}
const Screen& display( std::ostream &os ) const{
do_display(os); return *this;
}
private:
void do_display( std::ostream &os ) const;
}
跟const this指针相关的另外一个关键字是mutable(可变数据成员),通过将数据成员声明成mutable的,const成员函数可以改变mutable成员,const对象的mutable成员也是可以被修改的。
3. 类作用域
类的成员可以在类的定义体之外定义,出现在类的定义体之外的成员定义必须指明成员出现在哪个类中。例如代码片段2中的第二个get函数,我们在类的定义体外定义该函数时,首先必须指明成员出现在哪个类中,之后的形参表和函数体便处于类的作用域中(可以看到,get( index, index )函数用到了Screen中定义的index类型、width和contents数据成员而不需要再次声明作用域)。但是,函数返回类型则不一定在类作用域中,因为它出现在成员名字前面,例如代码片段2中的get_cursor函数,其返回类型是Screen中定义的index类型,因此必须以完全限定的类型名Screen::index来指定所需的index是在类Screen中定义的名字。
4. 构造函数
从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。定义如下Sales_item类:
class Sales_item{
public:
Sales_item( const string& book ):isbn(book),units_sold(0),revenue(0.0){}
private:
string isbn;
int units_sold;
double revenue;
}
其构造函数与下面的构造函数是等价的:
Sales_item( const string& book ){
isbn = book;
units_sold = 0;
revenue = 0.0;
}
第一种构造函数称为构造函数初始化列表,其中的book是对string型的isbn的显式初始化式。无论有没有初始化式,执行构造函数之前要初始化isbn成员,执行第二种构造函数前,用string型的默认初始化方法即空串对isbn初始化,然后再执行构造函数时再用book对isbn进行赋值计算。这两种构造函数的不同之处在于,使用构造函数初始化列表的版本初始化数据成员,没有定义初始化列表的构造函数版本在构造函数体中对数据成员赋值。这个区别的重要性取决于数据成员的类型。没有默认构造函数的类类型成员,以及const或引用类型的成员,都必须在构造函数初始化列表中进行初始化:如果类不存在默认构造函数,没有定义初始化列表的构造函数在找不到默认方法对成员初始化;const或引用类型的成员一旦被初始化了,就不能在函数体中对它们重新赋值。
成员被初始化的次序就是定义成员的次序。一个类哪怕只定义了一个构造函数,编译器也不会再生成默认的构造函数。
类通常应定义一个默认构造函数,假设NoDefault类没有默认构造函数,却有一个接受一个string实参的构造函数,意味着:
1. 具有NoDefault成员的每个类的每个构造函数,必须通过传递一个初始的string值给NoDefault构造函数来显式地初始化NoDefault成员
2. 编译器将不会为具有NoDefault类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其NoDefault成员。
3. NoDefault类型不能用作动态分配数组的元素类型
4. NoDefault类型的静态分配数组必须为每个元素提供一个显式的初始化式
5. 如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
继续Sales_item的例子,在期待一个Sales_item类型对象的地方,我们可以使用一个string来产生隐式转换:
Sales_item item("1-111-111111-1");
string null_book="9-999-99999-9";
item.same_isbn(null_book);
如果不希望发生这种隐式转换,可以通过将构造函数声明为explicit。explicit关键字只能用于类内部的构造函数声明上,在类的定义体外部所做的定义上不再重复它。
5. 友元
友元机制允许一个类将对其非公有成员的访问权授予指定的类或函数。友元的声明以关键字friend开始,它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。
类的友元可是是另外一个类,也可以是某个类的特定成员函数,还可以是非成员函数。不必预先声明类和非成员函数,就可以将它们设为友元,但是,如果类的友元是某个类的特定成员函数,则必须先定义包含成员函数的类,才能将成员函数设为友元。我正是在这一点上折腾了好久。
下面这段代码:
#ifndef SALES_ITEM_H
#define SALES_ITEM_H
#include<string>
class Account{
public:
int total();
double total( class Sales_item* it );
};
class Sales_item{
friend class unknown;
friend bool compareItem( Sales_item item1, Sales_item item2 );
friend double Account::total( Sales_item* y );
public:
typedef std::string ISBN;
bool same_isbn( const Sales_item &rhs ) const
{ return isbn == rhs.isbn; }
explicit Sales_item( ISBN book="", int sold=0, double r=0.0 )
:isbn(book), units_sold(sold), revenue(r){ count++; }
double avg_price( double percent );
private:
static int count;
const static double percentOff = 0.8;
ISBN isbn;
unsigned units_sold;
double revenue;
};
int Sales_item::count = 0;
double Sales_item:: avg_price( double percent = percentOff ){
if( units_sold )
return revenue*percentOff/units_sold;
else
return 0;
}
double Account::total( Sales_item* it ){
return it->revenue*(Sales_item::percentOff);
}
#endif
上面总共提到了三个类,Sales_item,Account以及unknown,其中unknown只有声明,没有定义。
Sales_item总共声明了三个友元,其中unknown是一个类,compareItem是一个非成员函数,因此在没有预先声明的时候就可以将它们设为友元。而Account::total则不一样,它是类Account的一个成员函数,因此在声明友元时必须先定义类Account。然而total以一个Sales_item作为输入,几乎出现了是先有鸡还是先有蛋的问题。解决的方法是对Sales_item做前向声明,这时,对于Account的定义体来说,只知道Sales_item是一种类类型,但不知道它有什么成员,因此total函数的实际定义没办法在类定义体内说明。上面的情况是我们把两个类的声明跟定义都放在一个文件里了,如果分别把两个类拆开,关系则更加复杂了。貌似没办法把Account的类定义体和函数定义放在同一个文件里。因为Sales_item的类定义体依赖于Account的类定义体,而Account的函数定义又依赖于Sales_item的的类定义体,形成了一个死循环。我找到的解决方法就是把Account的类定义体跟函数定义分开,Account的函数定义文件还需要include "Sales_item.cpp"。不知道是否还存在其他方法?
6. static类成员
上面这段代码同时也让我们看到了static类成员的一些特性。
首先是static数据成员必须在类定义体的外部定义,在类定义体内部只做声明。在像普通数据成员,static成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。如果没有第31行的定义,当我们定义一个类对象的时候,就会出现Undefined reference to "Sales_item::count"的错误。如果我们在类定义体内给count赋值,编译同样也会报错。例外的是,const static数据成员就可以在类的定义体中进行初始化。(书中说const static数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义,但是我没有在定义体外再定义percentOff,也没有出什么问题。)
由于static成员不是类对象的组成部分,static数据成员有下面一些普通数据成员不能支持的特性:
1. static数据成员的类型可以是该成员所属的类类型(非static成员被限定声明为其自身类对象的指针或引用)
2. static数据成员可以用作默认实参(如上面代码的第33行)
最后
以上就是大力斑马为你收集整理的C++ premier -- 类的全部内容,希望文章能够帮你解决C++ premier -- 类所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复