effective_c++(第三版)读书笔记
Effective C++ 读书笔记
条款02: 尽量以const,enum,inline替换#define
结合more effective c++可知#defines显然没有所谓private #define这样的东西。当然const成员变量是可以被封装。
举个例外当你在class编译期间需要一个class常量值,若不允许“static整数型class常量”完成“in class初值设定”,可
改用所谓的“the enum hack”补偿做法。
class GamePlayer{
private:
enum{NumTurns = 5};
int score[NumTurns];
}
enum hack的行为某方面说比较像#define而不像const,取一个const地址是合法的,但是取一个#define的地址就不合法,取一个enum的地址也通常不合法,你不想让别人获得一个pointer或reference指向某个整数变量,enum可以帮助你实现约束
对于单纯常量,最好以const对象或enums替换#defines
对于形似函数的macros,最好改用inline函数替换#defines
条款03: 尽可能使用const
const的一件奇妙的事情,它允许你指定一个语义约束,而编译器会强制实施这项约束
const可以用来外部修饰global或namespaces作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象
const可以用于函数返回值,各参数、函数自身产生关系
class TextBlock{
public:
const char & operator[] (std::size_t position) const{
return text[position];
}
char & operator[] (std::size_t position){
return text[position];
}
private:
std::string text;
}
TextBlock tb("hello");
std::cout << tb[0]; //调用non-const
const TextBlock ctb("world");
std::cout << ctb[0]; //调用const
我们可以通过这里与另一篇博客的读书笔记对照一下
即bitwise-constness和logical constness标准
举个例子如下
class CTextBlock{
public:
char & operator[](std::size_t position) const{//bitwise const 声明
return pText[position]; //但其实不适当
}
private:
char* pText;
};
所谓的logical constness的标准就是一个const成员函数可以修改它所处理的对象中的某写bit,但是只有在客户端检测不出来的情况下,才得如此 举个例子,如下
class CTextBlock{
public:
std::size_t length() const;
private:
char * pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const{
if(!lengthIsValid){
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
/*
但是上述确实不行的 因为编译器符合的标准是bitwise的标准 那么我们可以将对应的变量声明为mutable来解决问题
比如将
std::size_t textLength;
bool lengthIsValid;
声明为如下形式
mutable std::size_t textLength;
mutable bool lengthIsValid;
*/
举个另外解决的方案
class TextBlock{
public:
const char & operator[](std::size_t position) const{
return text[positon];
}
char & operator[](std::size_t position){
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
};
/*
如你所见 这个代码有两个转型动作,而不是一个 我们打算让non-const operator[]调用自己的const兄弟
如果内部单纯调用[]的话,那么会递归调用自己,为了避免无限递归,我们得明确指出调用的是const operator[]
所以涉及了两次的转型操作, 使用static_cast进行强制类型转换 转换为对应的const TextBlock& 再通过const_cast
移除了对应的常量性质
*/
条款04: 确定对象被使用前已经被初始化
初始化的责任落在了construct身上 保证对象的每一个成员都初始化
赋值与初始化
举个例子
ABEntry::ABEntry(const std::string & name, const std::string & address, const std::list<PhoneNumber> &phone){
the Name = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;//这些都是对应的assignments
}
//比较优秀的写法
ABEntry::ABEntry(const std::string & name, const std::string & address, const std::list<PhoneNumber> &phone)
:theName(name),theAddress(address),thePhones(phones),numTimesConsulted(0)
{
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;//这些都是对应的assignments
}
//class的成员变量总是按照以其声明的顺序被初始化的
不同编译单元的non-local static对象的初始化次序。
所谓的static对象,其寿命被构造出来直到程序结束为止,因此stack和heap-based对象都会被排除,这种对象包括global,定义为namespace作用与的对象,在class内,在函数内,以及在file作用域内被声明的static的对象。函数内的static对象称为local static对象,其他static对象被称为non-local static
举个例子
class FileSystem{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;
/* 假装这里有条分割线 */
class Directory{
public:
Directory( params );
};
Directory::Directory( params ){
std::size_t disks = tfs.numDisks();
};
Directory tempDir(params);
/*
c++针对于不同编译单元的non-local static对象 的初始化次序并无明确定义
显然解决方案是使用一个函数来调用这个non-local static对象,当然你把它放到了函数中
自然而然 这个non-local static -> local static
这个手法的实现基于:c++保证函数内的loacl static对象会在“该函数被调用期间” “首次遇到该对象定义式”时被初始化
*/
条款05:了解c++默默编写并调用了哪些函数
编译器一般会提供默认的construct,copy construct 跟 copy assignment;如果你声明了对应的构造函数,那么编译器就不再为它构造default构造函数,默认的拷贝构造函数会以“拷贝no1.objectValue内的每一个bits来进行对应的初始化”
编译器生成的copy assignment操作符,其行为基本上与copy构造函数如出一辙,但是会存在一些情况使得编译器拒绝为class生出operator=
举个例子如下
template<class T>
class NamedObject{
public:
NamedObject(std::string &name, const T& value);
private:
std::string &name;
const T objectValue;
};
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s;
/*
显然编译器拒绝编译赋值,因为对应的一个reference引用是不允许被改变的,但是对应的默认拷贝构造函数,会将每个bits都拷贝过来,更改const也是被拒绝的,所以这样的行为下建议自己编写代码
还存在最后一种情况,即base class的copy assignment是对应的private,然而对应的子类的copy assignment显然是不能被生成的。
*/
拓展一个对应的知识点
深拷贝与浅拷贝
参考博客
举个例子
struct Student
{
string name;
int age;
};
int main()
{
struct Student stu = {"liming", 18};
struct Student stu2 = {"wanger", 20};
stu2 = stu;
cout<<"age is : "<< stu2.age <<endl;
cout<<"name is :"<< stu2.name<<endl;
}
/*
显然这个拷贝是成功的结果也对应变化了
对应的output
age is : 18
name is :liming
再看看下面的例子
*/
struct stu
{
int i;
char c;
char* p;
};
int main()
{
struct stu s1,s2;
char * str = "rabbit is cute";
s1.i = 345;
s1.c = 'y';
s1.p = (char*)str;
s2 = s1;
printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p);
printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p);
}
/*
output:
s2 345, y, rabbit is cute
s1 ptr: 7934, s2 ptr : 7934
对应的我们可以看到s1跟s2的ptr指针指向同一块地址
那么我们可以知道对于s1的ptr更改 也等价于 对s2的ptr更改
这说明指针的并没有将内容复制一块给新指针来指向,只是让新指针指向原来的那个内存,这样就相当于,指针在这个复制的过程中只是复制了地址,而不是内容。
缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标--浅拷贝。
当然通过上述的描述我们可以修改代码为如下的形式
*/
struct stu
{
int i;
char c;
char* p;
stu operator=(stu& stuTmp)
{
i = stuTmp.i;
c = stuTmp.c;
p = new char(strlen(stuTmp.p) + 1);
strcpy(p, stuTmp.p);
return *this;
};
};
int main()
{
struct stu s1,s2;
char * str = "rabbit is cute";
s1.i = 345;
s1.c = 'y';
s1.p = (char*)str;
s2 = s1;
printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p);
printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p);
}
/*
测试代码如下:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;
class stu
{
public:
int i;
char c;
char* p;
stu operator=(stu& stuTmp)
{
this->i = stuTmp.i;
this->c = stuTmp.c;
this->p = new char(strlen(stuTmp.p) + 1);
for (int i = 0; i < strlen(stuTmp.p); i++)
{
this->p[i] = stuTmp.p[i];
}
//strcpy(pp, stuTmp.p);
return *this;
};
};
int main()
{
struct stu s1, s2;
char * str = "rabbit is cute";
s1.i = 345;
s1.c = 'y';
s1.p = (char*)str;
s2 = s1;
printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p);
printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p);
cin.get();
}
/*
output:
相当于重载operator=方法,这样还是运行,产生的结果就是这样的:
s2 345, y, rabbit is cute
s1 ptr: 7910, s2 ptr : 1050000
此时s1和s2中的指针p指向了不同的地址,可以打印一下此时这两个指针的内容是否一样,加入一下代码:
printf("s1 ptr: %s, s2 ptr : %s\n ", s1.p, s2.p);
产生的结果是:s1 ptr: rabbit is cute, s2 ptr : rabbit is cute
此时s1和s2中的p指针地址不同,但是指向的内容一致,所以这拷贝成功。
其实类的结构和上面的结构体是类似的,其实可以将结构体看成一个类来处理,结构体也可有自己的构造、析构、重载运算符河函数,可以简单的认为结构体是类的一种形式。
拷贝有两种:深拷贝,浅拷贝
当出现类的等号赋值时,会调用拷贝函数 在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。 但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。 所以,这时,必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。 简而言之,当数据成员中有指针时,必须要用深拷贝。
建议:
我们在定义类或者结构体,这些结构的时候,最后都重写拷贝构造函数,避免浅拷贝这类不易发现但后果严重的错误产生。
*/
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
你能想到的第一条方法就是使得对应的函数声明为private,那么使得成功阻止人们使用它,但是这种做法并没有绝对安全
因为对应的menber function和friend函数还是可以调用对应的private
当然我们有了上述的class定义,当客户企图拷贝HomeForSale对象,编译器会阻止他,如果你不慎在member函数或friend函数之内这么做,轮到连接器发出抱怨
只要将copy构造函数和copy assignment操作符声明为private就可以办到,但不是在HomeForSale只身,而是在一个专门为了阻止copying动作设计的base class内
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable & operator= (const Uncopyable&);
};
class HomeForSale: private Uncopyable{
};
/*
这样的处理是非常好的,因为任何人 甚至是member或者friend函数 尝试拷贝HomeForSale对象 编译器会试着生成一个对应的copy constructor或者一个copy assignment操作符,而正如条款12所说,这些函数的“编译生成版”会常会调用其base class的对应兄弟,而对应的那些调用会被编译器拒绝,因为其base class的拷贝函数是private
*/
条款07:为多态基类声明virtual deconstruct function
C++中,当derived class对象被一个对应的指向的base class point delete的时候,那么该base class带了一个non-virtual deconstruct 那么这是未定义行为,实际执行的时候通常发生的是对象的derived成分没被销毁。
当然这会造成一种诡异的“局部销毁”对象,显然这是形成资源泄漏的一种行为。
当然解决这种问题是非常简单的,你可以给出一个对应的virtual deconstructor function
举个例子
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
};
TimeKeeper * ptk = getTimeKeeper();
delete ptk;
如果一个类class不作为base class的话,令其析构函数为virtual往往是个馊主意
class Point{
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
/*
我还以为有什么高大上的理由,其实对应的理由就是因为使用virtual会导致出现vptr与vtbl,这样会导致增加一个指针的大小,那么会出现对应的字节对齐来导致空间变大
*/
注意不要去继承对应的STL,因为这些容器的deconstruct 都是对应的non-virtual deconstruct
谈谈对应的deconstruct的运作方式,最深层派生的那个class其析构函数最先被调用,编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。如果不这么做,连接器会发出对应的抱怨。
条款08: 别让异常逃离析构函数 (隐约记得在more effective c++中有讲的)
条款09:决不在构造和析构过程中调用virtual函数
显然还是举例子吧
class Transaction{
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction::Transaction(){
logTransaction();
}
class BuyTransaction: public Transaction{
public:
virtual void logTransaction() const;
}
class SellTransaction: public Transaction{
public:
virtual void logTransaction() const;
};
BuyTransaction b;
/*
显然这里有个BuyTransaction构造函数被调用,但首先Transaction构造函数一定会被更早的调用,那么derived class对象内的base class成分会在derived class只身成分被构造前先被构造,但是Transaction构造函数的最后一行调用virtual函数logTransaction,这个时候调用的logTransation是logTransaction内的版本,并不是BuyTransaction的版本
好吧,这个问题的出现其实就是因为base class构造期间virtual函数不会下降到对应的derived classes阶级
在base class constructor期间 virtual函数不是virtual函数
显然这是有理由的
因为base class的构造函数在执行的时候,derived class的成员肯定还没有初始化,如果这个期间的virtual函数下降到对应的derived class,要知道derived class的函数几乎是肯定会使用local 成员变量的,那么这些成员变量肯定还没有初始化
然后候捷大佬说:这是一张通往不明确行为和彻夜调试大会的直达车票(xswl)
看完了construct调用virtual 我们接下来看看deconstruct调用virtual
我们熟知析构函数是从最深的derived class进行对应的析构,如果在base class的deconstruct对应有virtual,那么derived class析构已经执行完毕,那么对象内的derived class 成员变量便呈现未定义值,所以c++调用不到,如果可以访问的话,那么就出现对应的问题了,所以进入base class析构函数后对象也就成为了一个base class对象,而c++的任何部分包括virtual函数,dynamic_cast等等也都是如此看待他的
当然会存在方案来解决这个问题 具体看书哈
令derived class将必要的构造信息向上传递到base class构造函数
*/
条款10:令operator=返回一个reference to *this
关于赋值可以写成,连锁的形式
int x, y, z;
x = y = z = 25;
x = (y = (z = 15));
//为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参
class Widget{
public:
.....
Widget& operator=(const Widget & rhs){
...
return* this;
}
}
class Widget{
public:
Widget& opertor+=(const Widget& rhs){
...
return *this;
}
Widget & operator= (int rhs){
return *this;
}
}
条款25: 考虑写出一个不抛异常的swap函数
namespace std{
template<typename T>
void swap(T &a, T &b){
T temp(a);
a = b;
b = temp;
}
}
这里看得我头痛 顺手记录一个max的模版
template<typename T>
inline const T& std::max(const T &a, const T& b){
return a > b ? a : b;
}
条款40: 明智而审慎地使用多重继承
举个对应的例子
class BorrowableItem{
public:
void checkOut();
};
class ElectronicGadget{
private:
bool checkOut() const;
};
class MP3Player:
public BorrowableItem,
public ElectronicGaget{};
MP3Plater mp;
mp.checkOut();
其实通过观察这段代码 我们可以发现例子中对于checkOut的调用是有歧义的
因为两个checkOut是有相同的匹配程度
显然存在一种解决的方案即
mp.BorrowableItem::checkOut();
如果继承体系出现较多的相通的路径 那么就有可能相同的基类通过 相通的路径进行对应的复制
当然解决方案是使用对应的“virtual 继承”
显然virtual继承是有对应的要付出的代价的
vitual base classes 初始化的规则比起non-virtual bases的情况远为复杂 并且不直观
vitual base的初始化责任是有继承体系中最低成的class复杂的
1)class若派生至virtual bases而需要初始化,就必须认知其virtual base 无论距离bases距离多远
2)当一个新的derived class加入继承体系中,他必须承担virtual bases的初始化责任
extern学习