C++Primer第五版【笔记】——第七章 类
2013-05-18 09:44
316 查看
类是数据的抽象和封装。数据抽象是一种将接口和实现分离的设计技术。接口是指用户可以对类使用的操作集。实现包括类的数据成员和接口函数体。封装使得类的使用者不必关注类内部是如何实现的,因为这些是类的设计者需要关注的。
在调用成员函数时,系统会自动传递类实例的地址给this指针:
【注】声明为const的类对象const CTest cc;,在调用构造函数初始化对象以后,才具有const的属性。所以构造函数不能声明为const。
如果没有显式定义构造函数,编译器会自动生成一个不带参数的合成默认构造函数。该默认构造函数所做的事是使用默认机制初始化数据成员。即:
如果有类内初始化,则使用类内的初始化值初始化。
否则,对于内置类型初始化为默认值(与局部变量初始化方式一样,一般为未知的值,所以最好进行初始化),对于类类型则调用该类的构造函数进行初始化。
当定义有构造函数时,编译器不会生成合成默认构造函数,如果想将不含参数的构造函数设置为和合成默认构造函数一致,可以在函数后添加= default。
既然编译器可以自动合成,为什么要自己定义构造函数?原因有三:
只有当没有自定义构造函数时,编译器才会自动合成;
合成默认构造函数的行为有时候并不是我们想要的;
有时候编译器不能自动生成构造函数。比如当某个类类型数据成员具有自定义构造函数时。
private:私有成员只能被类内部成员或者标识有friend的类或函数访问。private封装的类的实现。
封装的好处:
可以防止用户无意中改变类中封装的数据成员的状态。
将成员函数的实现封装起来,可以方便后续的修改和维护。因为只要类的接口不变,类内部封装部分的修改,不会导致用户代码的更改。当然,修改后的源文件需要重新编译。
inline函数的定义和类的定义应该放在同一个头文件中。
先定义CFriend类,go()函数需要声明但是不能定义;因为在go()函数可以使用CTest类的成员之前,CTest类必须定义。
定义CTest类,包含friend声明。
定义go()函数。
【注】friend声明只影响可访问性,不能代替一般意义上的声明。
在该名字所在的程序块内查找。只考虑在该名字之前出现的声明。
如果没有找到,则在程序块外查找。
如果没有找到,则程序出错。
而类的成员函数中使用的名字的查找规则有些不同:
首先,在函数体内,该名字出现之前查找它的声明。
如果在函数体内没有找到,则在类中查找。
如果在类中也没有找到,则在该函数定义出现之前查找。
可见成员函数的定义是在所以声明之后才编译,所以成员函数可以使用类内部任意位置出现的名字。
上面的规则只适用于成员函数体内的名字查找,对于函数参数列表和函数返回值中的名字查找规则,和一般的规则一样。
编译器不是按照参数初始化列表中变量定义的顺序进行初始化,而是根据变量在类中定义的顺序初始化。没有出现在初始化列表中的变量会默认初始化。
新标准扩展了构造函数初始化的使用。在委托构造函数的初始化列表中,只有一个调用另一个构造函数的入口。
发生默认初始化的情况:
在程序块范围内定义的非静态变量或数组没有初始化时
当一个类中包含有使用合成默认构造函数的类类型的成员时
当类类型成员没有在构造函数初始化列表中显式初始化时
发生值初始化的情况:
当初始化一个数组,提供的初始化少于数组长度时
当定义了一个没有初始化的局部静态对象时
当明确需要值初始化时(比如vector使用一个参数的构造器,用一个参数指定元素的个数)
【注】在一个表达式中只允许一次隐式类类型转换。
有时我们并不想让这种隐式的类类型转换发生。使用explicit声明可以终止这种隐式转换。
使用explicit声明的构造函数,只能使用直接初始化,不能使用赋值形式的初始化。
库函数中string的,有一个const char*类型的构造函数不是explicit,vector有一个size参数的构造函数是explicit。
所以数据成员是公有的
没有构造函数
没有类内初始化
没有基类或虚函数
聚合类就好比是C语言中的struct结构体中加入了一些函数。
static成员函数没有this指针,不能声明为const,在函数体内部也不能使用this。
static成员的引用可以通过类名+作用域操作符的方式,或者通过对象。
static数据成员的初始化一般和非内联函数的定义放在一起。
1 抽象数据类型
定义在类中的函数默认为内联的(inline)。类的成员函数必须在类内部声明,函数定义可以放在类的内部或外部。1.1 this指针
每一个类的内部都有一个隐含的this指针,该参数是由系统负责维护。它的类型是CLASSTYPE *const this;即指向某个类的const指针。所以this指针在初始化以后就不能改变。系统使用this指针来指明函数使用的是哪个实例的数据成员。在调用成员函数时,系统会自动传递类实例的地址给this指针:
CLASSTYPE exm; exm.func();可以将该函数调用理解为:CLASSTYPE::func(&exm);
1.2 const成员函数
正如1.1中所说,在调用成员函数时,会传递类实例的地址给this指针。如果该实例是const对象,那么非const指针是无法指向const对象的。可以在函数参数列表后加上const来表明是const成员函数。int CLASSTYPE::func() const;【注】const对象或指向const对象的指针或引用只能调用const成员函数。
1.3 构造函数
构造函数的作用是初始化类对象的数据成员。没有返回值。class CTest{ public: CTest() = default; CTest(int a1):a(a1){} int GetA() const {return a;} private: int a; };
【注】声明为const的类对象const CTest cc;,在调用构造函数初始化对象以后,才具有const的属性。所以构造函数不能声明为const。
如果没有显式定义构造函数,编译器会自动生成一个不带参数的合成默认构造函数。该默认构造函数所做的事是使用默认机制初始化数据成员。即:
如果有类内初始化,则使用类内的初始化值初始化。
class CTest{ ... private: int a = 0; // 类内初始化,c++11标准 };
否则,对于内置类型初始化为默认值(与局部变量初始化方式一样,一般为未知的值,所以最好进行初始化),对于类类型则调用该类的构造函数进行初始化。
当定义有构造函数时,编译器不会生成合成默认构造函数,如果想将不含参数的构造函数设置为和合成默认构造函数一致,可以在函数后添加= default。
既然编译器可以自动合成,为什么要自己定义构造函数?原因有三:
只有当没有自定义构造函数时,编译器才会自动合成;
合成默认构造函数的行为有时候并不是我们想要的;
有时候编译器不能自动生成构造函数。比如当某个类类型数据成员具有自定义构造函数时。
构造函数初始化列表
在执行函数体之前,编译器先根据构造函数初始化列表对每个数据成员初始化。如果省略了某个数据成员,且该成员在类内有初始化值,则根据类内的初始化值初始化,否则根据合成默认构造函数的规则对其初始化。2 访问控制和封装
2.1 访问控制
public:标识为public的成员可以被外部程序访问。一般用来定义接口。private:私有成员只能被类内部成员或者标识有friend的类或函数访问。private封装的类的实现。
class和struct的区别
class默认的访问权限是private,struct默认的访问权限是public。2.2 友元
如果想让某个类或外部函数访问类的私有成员,可以在类内添加friend声明,一般放在类的开头或结尾。class CFriend{ }; void go(); class CTest{ friend CFriend; friend void go(); public: CTest() = default; CTest(int a1):a(a1){} int GetA() const {return a;} private: int a; };【关于封装】
封装的好处:
可以防止用户无意中改变类中封装的数据成员的状态。
将成员函数的实现封装起来,可以方便后续的修改和维护。因为只要类的接口不变,类内部封装部分的修改,不会导致用户代码的更改。当然,修改后的源文件需要重新编译。
3 类的其他特性
3.1 有关类成员
3.1.1 在类中定义类型别名
在类内定义类型别名的方法:class CType{ public: typedef vector<int>::size_type viSize; };使用新标准的方法:
class CType{ public: using viSize = vector<int>::size_type; };类内部变量的声明可以不需要担心位置问题,而类型的定义一般需要放在类的开头,因为在使用类型前必须声明。
3.1.2 关于inline
在类内部定义的成员函数是默认inline的。当然也可以显式的声明一个成员函数为Inline。可以在类内声明,或者在类外定义时声明。一般将inline声明放在类外定义时。inline函数的定义和类的定义应该放在同一个头文件中。
3.1.3 mutable数据成员
声明为mutalbe的数据成员不会被const所限定,即使是声明为const的对象。所以,const成员函数可以改变mutable数据成员。3.2 返回*this的函数
返回*this的函数的返回值类型是类的引用。该类型返回值是左值。这种函数的好处是可以连写。比如:cout << "hello" << " world!" << endl;#include <iostream> using namespace std; class RThis { public: RThis &Get() { return *this; } int Go(int b) { return a = b; } int A() { return a; } private: int a; }; int main() { RThis obj1; obj1.Get().Go(10); cout << obj1.A() << endl; return 0; }对于上面的类的一个对象obj1,调用obj1.Get().Go(10); 则a的值会变为10。而如果Get()函数的返回值为RThis而不是引用,则a的值不会改变。因为*this返回后是赋值给一个临时对象,Go(10)改变的是临时对象中的a。
3.3 类类型
我们可以像声明内置类型一样声明一个类。class ClassDecl;此时的类是一个非完整类型(incomplete type)。我们可以定义一个指向该类型的指针或引用,可以声明一个包含非完整类型的参数或返回值的函数。但是,不能用非完整类型创建该类的对象。因为,编译器还不知道该类的内部成员,无法为对象分配内存空间。
class ClassDecl{ ClassDecl *pDec; // ok ClassDecl &rDec; // ok ClassDecl Dec; // error };
3.4 再说友元
前面提到过说可以在类中声明一个类或函数为friend。我们还可以声明一个类中的某个成员函数为其他类的友元。声明与定义的顺序要注意:class CFriend{ public: void go(); }; class CTest{ friend void CFriend::go(); }; void CFriend::go() { }
先定义CFriend类,go()函数需要声明但是不能定义;因为在go()函数可以使用CTest类的成员之前,CTest类必须定义。
定义CTest类,包含friend声明。
定义go()函数。
【注】friend声明只影响可访问性,不能代替一般意义上的声明。
4 类范围
在类外实现的成员函数需要在函数名前面指定类名和作用域符号,此后函数的参数列表,函数体都存在于该类的范围内。但是返回值是例外,需要单独指定。class CTest{ typedef unsigned UINT; public: UINT go(UINT); }; CTest::UINT CTest::go(UINT a) { return a; }
4.1 名字查找
当我们使用一个名字时,编译器需要查找与该名字对应的声明。查找方法:在该名字所在的程序块内查找。只考虑在该名字之前出现的声明。
如果没有找到,则在程序块外查找。
如果没有找到,则程序出错。
而类的成员函数中使用的名字的查找规则有些不同:
首先,在函数体内,该名字出现之前查找它的声明。
如果在函数体内没有找到,则在类中查找。
如果在类中也没有找到,则在该函数定义出现之前查找。
可见成员函数的定义是在所以声明之后才编译,所以成员函数可以使用类内部任意位置出现的名字。
上面的规则只适用于成员函数体内的名字查找,对于函数参数列表和函数返回值中的名字查找规则,和一般的规则一样。
class CTest{ public: UINT go(UINT); // error,UINT在使用前必须先声明 typedef unsigned UINT; };
5 再看构造函数
当我们定义一个变量时,一般会对它进行初始化,而不是在定义完之后对其赋值。int a = 1; // 定义并初始化 int b; // 定义,b被默认初始化,如果该定义出现在函数内部,则b的值未定义;如果出现在函数外部,则b=0 b = 1;我们知道如果一个数据成员没有出现在构造函数的初始化列表中,那么编译器会对其进行默认初始化
5.1 有时构造函数初始化器是必须的
初始化和赋值的区别在一般情况下可以不用计较。但是const和引用成员必须初始化,此时要和赋值区别开。同样的,没有默认构造函数的类类型的成员也必须初始化。类的初始化结束于构造函数体开始执行前,即必须在构造函数初始化列表中初始化。编译器不是按照参数初始化列表中变量定义的顺序进行初始化,而是根据变量在类中定义的顺序初始化。没有出现在初始化列表中的变量会默认初始化。
5.2 委托构造函数(Delegating Constructors)
【C++11】新标准扩展了构造函数初始化的使用。在委托构造函数的初始化列表中,只有一个调用另一个构造函数的入口。
class CTest{ public: CTest(int ia):a(ia){} CTest():CTest(0){} private: int a; };将初始化的任务由其他构造函数代理,这就是委托构造函数。
5.3 默认构造函数的职责
当一个对象被默认初始化或值初始化时,会自动使用默认构造函数(内置类型为默认初始化器)。发生默认初始化的情况:
在程序块范围内定义的非静态变量或数组没有初始化时
当一个类中包含有使用合成默认构造函数的类类型的成员时
当类类型成员没有在构造函数初始化列表中显式初始化时
发生值初始化的情况:
当初始化一个数组,提供的初始化少于数组长度时
当定义了一个没有初始化的局部静态对象时
当明确需要值初始化时(比如vector使用一个参数的构造器,用一个参数指定元素的个数)
class CFriend{ public: CFriend(int ib):b(ib){} private: int b; }; class CTest{ public: CTest(int ia):a(ia),cf(0){} private: int a; CFriend cf; };上面的例子中,如果在CTest的构造函数中,不显式的调用CFriend的构造函数初始化cf,则会发生错误。因为CFriend没有默认构造函数。所以总是提供一个默认构造函数是一个好习惯。
5.4 隐式类类型转换
每一个带有一个参数的构造函数都定义了一个隐式的类类型转换。这种构造函数也称为转换构造函数(converting constructor)。class Addor{ public: Addor(int ad):ad1(ad){} void add(const Addor &a) { ad1 += a.ad1;} private: int ad1; }; Addor addor(0); addor.add(10);当addor.add(10)调用时,常量10会先转换成addor类型,因为Addor类中定义了一个带有int参数的构造函数。
【注】在一个表达式中只允许一次隐式类类型转换。
有时我们并不想让这种隐式的类类型转换发生。使用explicit声明可以终止这种隐式转换。
使用explicit声明的构造函数,只能使用直接初始化,不能使用赋值形式的初始化。
class Addor{ public: explicit Addor(int ad):ad1(ad){} Addor(string str):s(str){} private: int ad1; string s; }; ... Addor addor1("ok"); // error,两次类类型转换 Addor addor2 = 10; // error,explicit构造函数 不能使用赋值形式的初始化虽然explicit声明的构造函数不能进行隐式类类型转换,但是可以使用static_cast显式转换。
库函数中string的,有一个const char*类型的构造函数不是explicit,vector有一个size参数的构造函数是explicit。
5.5聚合类
满足以下条件:所以数据成员是公有的
没有构造函数
没有类内初始化
没有基类或虚函数
聚合类就好比是C语言中的struct结构体中加入了一些函数。
struct Data{ int data; int total; int add(int a) { total += a; } };
6 static类成员
static类成员,包括数据成员和成员函数,属于类,而不属于一个特定的类对象。static成员函数没有this指针,不能声明为const,在函数体内部也不能使用this。
static成员的引用可以通过类名+作用域操作符的方式,或者通过对象。
class Test{ public: static void go(); }; void Test::go() { }static只需要在类内声明,在类外定义是不需要声明。
static数据成员的初始化一般和非内联函数的定义放在一起。
class Test{ public: static void go(); private: static int size ; }; int Test::size = 10; void Test::go() { }
相关文章推荐
- C++primer第五版第七章学习笔记
- C++primer第五版笔记-第十六章模板与泛型编程
- C++primer 第五版 *笔记三 [渣渣进化记]
- 《C++primer》 第五版 第一章笔记
- C++Primer 中文版 第五版 第七章课后习题答案
- C++primer第五版笔记-第十七章标准库特殊设施
- C++primer第五版第十二章学习笔记
- 《C++ primer》第五版 第七章 笔记
- C++primer第五版笔记-第七章类
- C++primer第五版第十章学习笔记
- C++primer第五版笔记-第九章顺序容器
- C++primer 第五版 *笔记二 [渣渣进化记]
- C++primer(第五版) 第一章 学习笔记
- C++primer第五版笔记-第十八章用于大型程序的工具
- C++Primer(第五版)学习笔记
- C++Primer第五版【笔记】——第八章 IO库
- C++primer第五版笔记-第十九章特殊工具与技术
- C++primer第五版第一章学习笔记
- C++primer第五版第三章学习笔记
- C++Primer第五版【学习笔记】——第二章 变量和类型