完整阅读C++ Primer Plus
系统重新学习C++语言部分,记录重要但易被忽略的,关键但易被遗忘的。
使用类
1、不能重载的运算符
1 sizeof sizeof运算符 2 . 成员运算符 3 .* 成员指针运算符 4 :: 作用域解析运算符 5 ?: 条件运算符 6 typeid 一个RTTI运算符 7 const_cast 强制类型转换运算符 8 dynamic_cast 强制类型转换运算符 9 reinterpret_cast 强制类型转换运算符 10 static_cast 强制类型转换运算符
2、只能通过成员函数重载的运算符
1 = 赋值运算符 2 () 函数调用运算符 3 [] 下标运算符 4 -> 通过指针访问类成员的运算符
3、关于类的类型转换函数,C++11支持对其使用explicit关键字,使其无法进行隐式类型转换。
4、对于定义了一个以上的转换函数的类,编译器在某些情况下(如将一个对象直接赋值给一个基本类型,或用cout输出时)无法确定应该使用哪一个转换函数(进行隐式类型转换),因此将出现二义性错误,但只有一个转换函数时,编译器只能选择这一个,因此不会出错。
类和动态内存分配
5、将新对象显示地初始化为现有对象时将调用拷贝构造函数,默认的拷贝构造函数将除静态成员以外的所有成员按值赋值。
1 String a(b); 2 String a = b; 3 String a = String(b); 4 String * a = new String(b);
将已有的对象赋值给另一个已有的对象时,会调用赋值构造函数。
6、静态成员函数不与特定的对象关联,因此只能使用静态数据成员(单例模式)。
7、对于使用定位new运算符创建的对象,应显式地调用其析构函数,需要注意的是,在析构时,对象的析构顺序应该与创建顺序相反,因为晚创建的对象可能依赖于早创建的对象,另外,只有当所有对象被销毁后,才能释放存储这些对象地缓冲区。
8、只有构造函数可以使用初始化列表语法,对于const类成员(C++11之前)和声明为引用的类成员,必须使用这种语法,因为它们只能在被创建时初始化。
类继承
9、 公有继承是最常用的继承方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。公有继承不建立has-a关系;公有继承不建立is-like-a关系;公有继承不建立is-implemented-as-a(作为……来实现)关系;公有继承不建立uses-a关系。在C++中,完全可以使用公有继承来实现has-a、is-implemented-as-a或use-a关系,然而这样做通常会导致编程方面的问题,因此,还是坚持使用is-a关系吧。
10、 在基类的方法中使用关键字virtual可使该方法在基类已经所有派生类(包括从派生类派生出来的类)中是虚的,也就是说只要函数名相同,只需要在基类中声明为虚函数,那它的派生类中,包括派生派生类中的这个函数都是虚函数,但为了可读性,一般派生类中的虚函数也用virtual声明。
11、如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。
12、如果基类声明被重载了,则应在派生类中重新定义所有的基类版本,如果只在派生类中只定义了一个版本,则另外的版本将被隐藏。
13、C++允许纯虚函数有定义,但不能在类内定义,可以在实现文件中定义。
14、当基类和派生类都为至少一个成员采用了动态内存分配时,派生类的析构函数,拷贝构造函数,赋值构造函数都必须使用相应的基类方法来处理基类元素。对于析构函数,这是自动完成的;对于拷贝构造函数,是通过初始化列表中调用积累的拷贝构造函数完成的;对于赋值构造函数是通过使用作用域解析运算符显示地调用基类的赋值构造函数完成的。
15、当派生类的友元函数需要访问基类中的非公有成员时,做法是在派生类的友元函数中将派生类的引用强制类型转换为基类的引用。
C++中的代码重用
16、当类的初始化列表包含多个项目时,这些项目的初始化顺序为声明它们的顺序,而不是他们在初始化列表中的顺序,如果代码使用一个成员的值作为另一个成员初始化表达式的一部分时,初始化顺序就需要引起注意。
17、在继承时,private是默认值,因此忽略访问限定符也将导致私有继承。
18、在私有继承时,访问基类方法,需要使用类名加作用域解析运算符访问;访问基类对象(例如将基类对象当作返回值时),可以将派生类强制类型转换为基类;访问基类友元函数时,因为友元函数不属于成员函数,因此不能显式地限定函数名去访问,可以通过显式地转换为基类来调用正确的函数。
19、通常,应该使用包含来建立has-a关系,如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
20、对于指向对象的类或引用中的隐式向上转换,公有继承直接支持,保护继承只在派生类中支持,私有继承不支持。
21、如果要使私有继承的基类中的私有函数可以在派生类外访问,可以声明一个公有函数,再去调用基类的私有函数,另外还可以使用using声明:
1 class A:private B 2 { 3 public: 4 using B:foo; // 只需要有函数签名即可,所有重载版本都可以使用 5 }
另一种老式的方法是将基类方法名放在派生类的共有部分。
22、在多重继承中,如果出现了菱形继承,顶端的基类应该使用虚继承,防止底端的派生类包含两份基类,同时,在构造函数的初始化列表里应该显式地调用顶端基类的构造函数,对于虚基类必须这样做,否则将使用虚基类的默认构造函数,并且此时的虚基类无法通过中间的派生类去完成构造,但对于非虚基类,这是非法的。
23、在混合使用虚基类和非虚基类,类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含,一个表示所有的虚途径即基类子对象和分别表示各条非虚途径的多个基类子对象。
24、使用非虚基类时,二义性的规则很简单,只要类从不同类那里继承了同名的成员,使用时没有用类名限定,则一定会导致二义性,但虚基类时不一定会导致二义性,如果某个名称优先于其他名称,则使用它。优先的规则是,派生类中的名称优先于直接或间接祖先类中的相同名称。
25、有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类是非法的。
26、 在类模板中,模板代码不能修改参数的值,也不能使用参数得地址,在实例化时,用作表达式参数的值必须是常量表达式。
27、使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化,声明必须位于模板定义所在的命名空间中。
28、C++允许部分具体化:
1 template <class T1, class T2> class Pair{}; // 一般版本 2 template <class T1> class Pair<T1, int>{}; // 部分显式具体化版本
template后的<>中是没有被具体化的参数,如果指定所有的类型,template后的<>将为空,这会导致显式具体化。
29、如果有多个版本可以选择,编译器将选择具体化最高的版本,部分具体化特性使得能够设置各种限制。
1 template<T> class Feeb{}; 2 template<T*> class Feeb{}; // 也可以为指针提供特殊版本来部分具体化 3 4 template<class T1, class T2, class T3> class Trio{}; //一般版本 5 template<class T1, class T2> class Trio<T1, T2, T2>{}; // 使用T2设置T3 6 template<class T1> class Trio<T1, T1*, T1*>{}; // 使用T1的指针来设置T2和T3
30、较老的编译器不支持模板成员,而另一些编译器支持模板成员,但是不支持在类外面的定义。如果支持类外定义,则必须通过作用域解析运算符指出是哪个类的成员,并且使用嵌套模板的声明方式。
1 template<typename T> 2 template<typename V> // 嵌套方式
31、 对于模板类的非模板友元函数,它不是通过对象调用的,因为它不是成员函数,它可以访问全局对象,可以使用全局指针访问非全局对象,可以创建自己的对象,可以访问独立于对象的模板类静态数据成员。
32、在声明模板类的约束模板友元函数(友元函数也是模板函数)时,需要在类中声明具体化的友元函数,同时也需要在类外声明并且给出友元函数的定义。
33、除了使用typedef对模板进行重命名,C++11新增了别名。
1 template<typename T> 2 using arrtype = std::array<T,12>; 3 arrtype<int> days;
这种语法也适用于非模板,用于非模板时,它和typedef等价。