大神们常说不要重复造轮子,小编说不要重复踩坑,希望小编踩过得坑大家不要再踩。

item2:
宏坑多,尽量用const和enum。类中定义的 static const int val = 0;
只是声明,如果要取它的地址必须在实现文件中定义,定义不能有赋值,因为声明里面已经赋值了。

泛型和面向对象C++

1.
在类内部定义的函数默认为inline,内联函数应该在头文件中定义,因为其定义对编译器必须是可见的,以便编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。

2.assert

3.异常

4.由于流对象不能复制,因此不能存储在容器中;由于流不能复制,因此形参或返回类型也不能为流类型,必须用指针或引用,对IO对象的读写会改变它的状态,因此引用必须是非const的。

5.如果需要重用文件流读写多个文件,必须在读另一个文件之前调用clear清除该流的状态。

6.前向声明。在声明之后,定义之前,类是一个不完全类型,即已知它是一个类型,但不知道包含哪些成员。不完全类型只能以有限方式使用。不能定义该类型的对象。不完全类型只能用于定义指向该类型的指针和引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。在创建类的对象之前,必须完整地定义该类。必须定义类,而不只是声明类,这样,编译器就会给类的对象预定相应的存储空间。同样地,在使用引用或指针范文类的成员之前,必须定义类。

7.不能从const成员函数返回指向类对象的普通引用。const成员函数只能返回*this作为一个const引用。

8.引用全局变量

int height;
void dummy(int height)
{
    ::height = 1;
}

函数中设定全局变量height为1而不是参数height为1.

 

9.必须对任何const或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。

10.按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。

11.实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的。通常在默认构造函数中给成员提供的初始值应该指出该对象时“空”的。

12.友元不是授予友元关系的那个类的成员,所以它们不受其声明出现部分的访问控制影响。通常,将友元声明成组地放在类定义的开始或结尾是个好主意。友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。

13.static函数没有this指针,不是任何对象的组成部分,不能声明为const,不能声明为虚函数。

14.static数据成员必须在类定义体的外部定义

15.拷贝构造函数可用于初始化顺序容器中的元素,如

 

vector svec(5);

编译器首先使用string默认构造函数创建一个临时值来初始化svec,然后使用拷贝构造函数将临时值复制到svec的每个元素。

 

16.为了防止复制,类必须显式声明其拷贝构造函数为private。如果想要连友元和成员的复制也禁止,就可以声明一个private拷贝构造函数但不对其定义,这样在链接时导致错误。

17.不允许复制的类对象只能作为引用传递给函数或从函数返回,它们也不能用作容器的元素。

18.容器中的元素总是按照逆序撤销,先撤销下标为size()-1的元素,最后是下标为0的元素。

19.三法则:指的是如果需要析构函数,则也需要赋值操作符和拷贝构造函数。

20。合成析构函数并不删除指针成员所指向的对象。

21.析构函数没有返回值,没有形参。因为不能指定任何形参,所以不能重载析构函数。虽然一个类可以定义多个构造函数,但只能提供一个析构函数,应用于类的所有对象。

22.析构函数与拷贝构造函数或赋值操作符之间的一个重要区别是,即使我们编写了自己的析构函数,合成析构函数仍然运行。如我们编写了一个空的析构函数,则类的各成员还可以被合成析构函数撤销。合成析构函数在自定义析构函数之后执行。

23.大多数C++类采用以下三种方法之一管理指针成员:

(1)指针成员采取常规指针型行为。这样的类具有指针的所有缺陷但无需特殊的复制控制。

(2)类可以实现所谓的“智能指针”行为。指针所指向的对象是共享的,但类能够防止悬垂指针。

(3)类采取值型行为。指针所指向的对象时唯一的,由每个类对象独立管理。

24.不能重载的操作符

:: .* . ?:

25.重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类型对象的操作符含义。

26.操作符的优先级、结合性或操作数数目不能改变。除了函数调用操作符operator()之外,重载操作符时使用默认实参是非法的。

27.作为类成员的重载函数,形参看起来比操作数少1.作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数。一般将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员。

28.重载逗号、取地址、逻辑与、逻辑或等操作符通常不是好做法。这些操作符具有有用的含义,如果我们定义了自己的版本,就不能使用这些内置含义。

29.当一个重载操作符含义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。

30.管理容器键类型和顺序容器的类型应定义==和<操作符,理由是许多算法假定这些操作符存在。例如sort算法使用<操作符,find算法使用==操作符。

31.类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用。

32.类型转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。例如,operator
int返回一个int值。转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为const成员。

33.类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。

34.派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。

35.派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归。

36.private继承时可以在派生类的public部分使用using
Base::XX的形式,使得基类的私有成员可以被用户访问。

37.使用class保留字定义的派生类默认具有private继承,而用struct保留字定义的类默认具有public继承。

38.友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。

39.如果基类定义了static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。

40.构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口。派生类构造函数不能初始化基类的成员且不应该对其基类成员赋值。
41.与构造函数不同,派生类析构函数不负责撤销基类对象的成员,编译器总是显式调用派生类对象基类部分的析构函数。每个析构函数只负责清楚自己的成员。

42.即使没有工作要做,继承层次的根类也应该定义一个虚析构函数。

43.在复制控制中,只有析构函数可以定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。将赋值操作符定义为虚函数将在派生类中定义一个参数为基类对象的operator=,与派生类中赋值操作符不符合。因此,将类的赋值操作符定义为虚函数会令人混淆,而且不会有什么用处。

44.如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身定义类型的版本。

45.在继承情况下,派生类的作用域嵌套在基类作用域中。

46.对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,就像使用基类类型的引用或指针时可能发生的,静态类型仍然决定着可以使用什么成员。

47.在派生类作用域中派生类成员函数将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。

48.如果派生类想通过自身类型使用所有重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。若不想重定义所有,可以为重载成员提供using声明。一个using声明只能指定一个名字,不能指定形参表,因此可以将该函数的所有重载实例加到派生类的作用域。

49.虚函数必须在基类和派生类中拥有同一原型。

50.C++ primer
P501。派生类的同名函数可能会将基类的虚函数屏蔽,进而无法通过派生类对象调用,可以通过指向派生类对象的基类引用或指针调用。

51.因为派生类对象在赋值给基类对象时会被“切掉”,所以容器与通过继承相关的类型不能很好地融合。

52.面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。

53.用作模板形参的名字不能在模板内部重用,这一限制还意味着模板形参的名字只能在同一模板形参中使用一次。

54.在模板成员名前加上关键字typename作为前缀,可以告诉编译器将成员当作类型。

55.数组形参可以声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况去,数组大小成为形参和实参类型的一部分。编译器检查数组实参的大小与形参的大小是否匹配。

56.显式模板实参从左至右与对应模板形参相匹配。

57.当编译器看到模板定义的时候,它不立即产生代码。只有在看到用到模板时,如调用了函数模板或调用了类模板的对象的时候,编译器才产生特定类型的模板实例。

58.在包含编译模型中,编译器必须看到用到的所有模板的定义。

59.非类型模板实参必须是编译时常量表达式。

60.特化和部分特化可以具有与通用类模板完全不同的成员集合。

61.函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。

62.异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。

63.抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在所指向的对象。

64.栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。

65.在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。一般而言,terminate函数将调用abort函数,强制从整个程序非正常退出。

66.与析构函数不同,构造函数内部所做的事情经常会抛出异常,因此要保证适当地撤销已构造的成员。

67.如果找不到匹配的catch,程序就调用库函数terminate。

68.catch捕获的类型必须是已定义的,类型的前向声明不行。

69.通常,如果catch字句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。

70.如果catch(…)与其他catch子句结合使用,它必须是最后一个,否则,任何跟在它后面的catch子句都将不能被匹配。

71.构造函数要处理来自构造函数初始化式的异常,唯一的方法是将构造函数编写为函数测试块。

72.异常安全指即使发生异常程序也能正常操作,即被分配的任何资源都适当地释放。通过定义一个类来封装资源的分配和释放,可以保证释放资源。这一技术常称为“资源分配即初始化”,简称RAII。应该设计资源管理类,以便构造函数分配资源而析构函数释放资源。

73.autoi_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组。当auto_ptr被复制或赋值的时候,有不寻常的行为,因此,不能将auto_ptr存储在标准库容器类型中。auto_ptr的复制和赋值改变右操作数,因此,赋值的左右操作数必须都是可修改的左值。

74.应该只用get询问auto_ptr对象或者使用返回的指针值,不能用get作为创建其他auto_ptr对象的实参。

75.auto_ptr对象与内置指针的另一个区别是,不能直接将一个地址9或者其他指针)赋给auto_ptr对象

76.auto_ptr缺陷:

菲律宾太阳 1

77.如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。

78.在编译的时候,编译器不能也不会试图验证异常说明。如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected。默认情况下,unexpected函数调用terminate函数,terminate函数一般会终止程序。

79.因为不能再编译时检查异常说明,异常说明的应用通常是有限的。异常说明有用的一种重要情况是,如果函数可以保证不会抛出任何异常,对函数的用户和编译器都有所帮助。

80.派生类虚函数异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类异常说明不会增加新的可抛出异常。

81.在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格。

82.命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。命名空间作用域不能以分号结束。

83.命名空间可以在几个部分中定义。命名空间由它的分离定义部分的总和构成,命名空间是累积的。

84.未命名的命名空间与其他命名空间不同,未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。在命名空间引入C++之前,采用static声明局部于文件的名字。

85.如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体。

86.接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。

87.为了提供命名空间中所定义模板的自己的特化,必须保证在包含原始模板定义的命名空间中定义特化。

88.在虚派生中,由最底层派生类的构造函数初始化虚基类。无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。

89.sort排序默认使用less,为递增排序。

90.模板函数是函数模板的一个实例。

 

1.
在类内部定义的函数默认为inline,内联函数应该在头文件中定义,因为其定义对编译器必须是可见的,以便编译器能够…

模板类的编译和普通类是截然不同的,所以我们在写模板类的时候需要注意两个常见的语法问题。

item3:

  • ### 模板类的声明与定义要放在同一文件下。
const std::vector<int>::iterator iter    // acts like a T * const
std::vector<int>::const_iterator citer // acts like a const T *

简而言之,就是所有代码都要写在头文件.h中。举个例子,我们写一般类的做法是

重载运算操作符的返回值最好设成const。函数的const和非const是不一样的,可以重载。重载[]操作符最好重载两个版本。可以用下面这个方式来减少两个重载函数的重复代码

//demo_A.hclass demo_A{ void function_A();};//demo_A.cppvoid demo_A::function_A(){/*...*/}
const char& operator[](std::size_t position)const
{
    ...
    return text[position];
}
char& operator[](std::size_t position)
{
    return const_cast<char&>(
        static_cast<const TextBlock&>(*this)[position];
    )
}

但是,我们在写模板类的时候不能这样做,应该都写在头文件里

const成员函数不能保护成员指针指向的东西,如果要保护成员指针指向的东西,要用template写一个wrapper类,重载->操作符(具体看看二楼答案)。
用mutable声明的成员变量即使在const对象里面也能被修改。

//demo_A.htemplate class<T>class demo_A{ void function_A();};template class<T>void demo_A<T>::function_A(){/*...*/}

item4:
用初始化列表来初始化成员函数效率高。
用函数中的static对象来解决一个全局对象的初始化依赖另外一个文件的全局对象,而我们不能控制全局对象初始化顺序的问题,为了效率可以把这个函数inline。

为什么会这样呢,我的理解是,不同的模板是不同的类型,会生成不同的代码,函数的入口地址也会不一样。而编译器在模板使用之前是不知要要套用哪个类型的。所以模板类的实现,脱离具体的使用,是无法单独的编译的;把声明和实现分开的做法也是不可取的,必须把实现全部写在头文件里面。

item6:禁止拷贝的方法:将拷贝构造函数和赋值操作符设为private防止类外调用,不实现它防止friend调用,将它们声明在基类可以把错误信息从链接期移到编译期,可以直接继承boost的noncopyable。不过如果因此而多重继承,noncopyable这个空基类的空间优化可能会失去。

  • ### 模板类继承中,派生类访问基类保护对象要加范围解析操作符,或者加this->

item7:凡是用了虚函数的类一定要有虚析构函数。反正用了虚函数都要插多一个vptr,多个虚析构也不会增加成本。
std::string, vector, list, set,
tr1::unordered_map没有虚构造,所以不要继承他。

举个例子,我们写一般类的做法是

item8:不要在析构函数里面抛异常。如果遇到必须抛的情况,要处理在析构过程中抛异常的情况,应该留接口给类用户在析构抛异常的时候处理好析构。这种做法很丑,没有优雅解决办法的根源是,析构函数的语义就假设析构过程一定是没问题的。

class demo_Base{protected: int _a; };class demo_Derived: public demo_Base{ void functionB(){ _a = 1; //直接访问基类保护对象 }};

item9:不要在构造函数和析构函数里面调用虚函数,因为那个时候vptr指向基类的虚函数表。如果构造函数和析构函数是通过另外一个函数来调用虚函数,这种错误的做法不会被编译器检查出来。

但是在模板类中,这样做就会报错,无法识别变量_a。

item10:赋值操作符要返回*this引用,因为这是赋值运算符对于内置类型的语义。

方法一:我们要在_a前加范围解析操作符::

item11:

方法二:在_a前加this->
template class<T>class demo_Base{protected: T _a;};template class<T>class demo_Derived: public demo_Base<T>{ void functionB(){ demo_Base<T>::_a = 1; //方法一 this->_a = 1; //方法二 }};

我的理解是:因为派生类模板在具体套用类型之前,编译器实际上是不知道基类是谁的,找不到基类的定义与声明。因此,我们就用范围解析符或者this指针帮它找到基类中的对象。

Object& Object::operator=(const Object& rhs)
{
    Object *temp = p_obj;
    p_obj = new Object(*rhs.p_obj);
    delete temp;
    return *this;
}
Object& Object::operator=(const Object& rhs)
{
    Object temp(rhs);
    swap(temp);
    return *this;
}
Object& Object::operator=(const Object rhs)
{
    swap(rhs);
    return *this;
}

注意swap是自己定义的成员函数。上面这三种做法能够防止自己给自己赋值,因为delete放在后面,也可以防止new抛异常以后,p_obj指向空资源。

item12:记得在拷贝构造函数和赋值操作符里面调用 成员对象和基类
的拷贝构造函数和赋值操作符,不然编译器会给成员对象和基类调用默认拷贝构造函数而且不调用他们的赋值操作符。拷贝构造函数和赋值操作符有相同代码的时候,不应该让他们互相调用,而是将重复代码放在第三个函数里面。

item13:
用构造函数和析构函数的语义来管理资源,以防止多处return或者函数中间抛异常而引起资源泄漏的情况(RAII,Resources
Acquisition Is Initialization)。
auto_ptr不能用于STL容器,因为如果拷贝装着auto_ptr的容器,原来的容器里面的auto_ptr全部被设成null。但shared_ptr就没问题。但两个智能指针都不能用于数组。

发表评论

电子邮件地址不会被公开。 必填项已用*标注