1060人阅读
前言:
effective C++ 是一本经典之作,其翻译较为贴合原著,但读起来未免有些僵硬而让人摸不着头脑,所以,我会以更为贴近中国人的理解,对此书进行一些阐释。
条款01:把 C++ 看成一个语言联邦
C++由几个重要的次语言构成
C语言:区块,语句,预处理器,数组,指针等等。
类:class,封装,继承,多态......(动态绑定等等)
模板:涉及泛型编程,内置数种可供套用的函数或者类。
STL:STL是个模板库,主要涉及容器,算法和迭代器
在不同情况下使用适合的部分,可以使 C++ 实现高效编程
条款02:用const, enum, inline 替换 #define
1、#define 修饰的记号,在预处理的时候,已经全部被替换成了某个数值,如果出错,错误信息可能会提到这个数值,而不会提到这个记号。在纠错方面很花时间,因为其他程序员不知道这个数值代表什么。我们可以用 const 和 enum 解决这个问题。
-
//enum hack 补偿做法: -
enum 枚举量{para1 = value1, para2 = value2,......} -
//将一个枚举类型的数值当作 int 类型使用 -
//和 #define 很像,都不能取地址,但它没有 #define 的缺点
2、#define 不能定义类的常量,因为被 #define 定义的常量可以被全局访问,它不能提供任何封装性。
3、#define 修饰的宏书写繁琐且容易出错,inline 函数可以避免这种情况:
-
#define MY_COMPARE(a, b) f((a) > (b) ? (a) : (b)) -
//这是一个三目运算符,如果 a > b,则返回 a,否则返回 b -
//宏中的每一个实参都必须加上小括号 -
//调用: -
int a = 5, b = 0; -
MY_COMPARE(++a, b);//1 -
MY_COMPARE(++a, b + 10);//2 -
/* -
1式中,++a => a = 6 => 6 > b = 0 => return ++a; -
a 的值竟然增加了两次! -
*/ -
//定义 inline: -
#define MY_MAX(a, b) (a) > (b) ? (a) : (b) -
template<class T> -
inline int MY_COMPARE(const T&a, const T&b) -
{ -
a > b ? a : b; -
} -
//inline 将函数调用变成函数本体 -
//传入的是 ++a 的值 -
int main() -
{ -
int a = 2; -
int b = 2; -
MY_COMPARE(++a, b); -
cout << a << endl; -
//此时 a = 3 -
MY_MAX(++a, b); -
cout << a << endl; -
//此时 a = 5 -
system("pause"); -
return 0; -
}
条款03:尽可能使用 const
const 允许我们指定一个语义约束,使某个值应该保持不变
1、const 修饰 变量,指针,函数,函数返回值等,可以使程序减少错误,或者更容易检测错误:
指针常量:int* const p;//指针地址不可变,指针指向值可变
常量指针:const int* p;//指针指向值不可变,指针地址可变
常量指针常量:const int* const p;//都不可变
const 修饰迭代器:
iterator 相当于 T* const //指针常量
const_iterator 相当于 const T* //常量指针
const 修饰函数返回值:
-
const int max(int a, int b) -
{ -
a > b ? a : b; -
} -
int c = 6; -
max(a,b) = c; -
//将 c 的值赋给 max(a, b) 是没有意义的,const 防止这种操作的发生
2、const 修饰成员函数
如果两个成员函数只是常量性不同(其他相同)则可以发生重载
const 类对象调用 const 成员函数
non-const 类对象调用普通成员函数
bitwise:
const 成员函数不能改变(除 static)成员变量的值,因为常函数里 this 指针指向的值不可改变。同理,const 对象不可以调用 non-const 函数,因为函数有能力更改成员属性的值。
但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址(地址是 this 指向的值),则不算是 const 函数 ,但能够通过 bitwise 测试。
使用 mutable 可以消除 non-static 成员变量的 bitwise constness 约束。
-
class person -
{ -
public: -
person(int a) -
{ -
m_id = a; -
} -
int& func() const -
{ -
m_id = 888; -
} -
mutable int m_id; -
}; -
int main() -
{ -
const person p(666); -
p.func(); -
cout << p.m_id << endl; -
system("pause"); -
return 0; -
}
3、当 const 和 non-const 成员函数有实质的等价实现时,利用两次转型,令 non-const 调用 const 可以避免代码重复。
-
const char& operator[](int pos) const -
{ -
//... -
//... -
return name[pos]; -
} -
char& operator[](int pos) -
{ -
return -
const_cast<char&>//移除第一次转型添加的 const -
( -
static_cast<const classname>(*this)[pos] -
//把 classname 类型数据转换为 const classname -
//使得能够调用 const operator[] -
); -
}
条款04:确定对象被使用前已经被初始化
1、内置数据类型:
-
int a = 0; -
double b = 0; -
char* c = "A C-style string";
2、自定义数据类型:
使用成员初值列替换有参构造,且次序和class声明次序相同:
-
class person -
{ -
public: -
person(int age, int id, string name)//这是一个有参构造 -
{ -
this->m_age = age; -
this->m_id = id; -
this->m_name = name; -
} -
int m_age; -
int m_id; -
string m_name; -
}; -
//这是成员初始列 -
class person -
{ -
public: -
person(int age, int id, string name): -
m_age(age), -
m_id(id), -
m_name(name) -
{} -
int m_age; -
int m_id; -
string m_name; -
}; -
/* -
成员初始列格式: -
classname(parameter1, parameter2,...): -
member1(value1), -
member2(value2), -
... -
membern(value) -
{} -
*/
有参构造是先执行默认构造,再给成员变量赋值。这有一个坏处,const 修饰的量不可以被赋值,但是可以初始化。
使用成员初值列则省去了默认构造部分。成员初值列在不传参的情况下,可以看成默认构造。
-
int m_size; -
char m_array; -
//这两个成员变量在初始化的时候是有次序的,m_array 的大小是由 m_size 指定的 -
//所以必须先初始化 m_size -
//实际上,成员初值列中变量的次序可以和声明次序不同 -
//但为防止这类错误出现,成员初值列的次序应当和声明次序相同
3、使用 local-static 对象替换 non-local-static 对象:
函数内 static 对象是 local-static 对象,函数外 static 对象是 non-local-static 对象。
-
class teacher -
{ -
...... -
string tname(){...}; -
}; -
extern teacher t; -
//声明一个 teacher 对象 t, 预留给用户使用 -
//不定义是因为我们不知道什么时候用它 -
class student -
{ -
...... -
student(params) -
{ -
string a = t.tname(); -
//使用 t 对象 -
} -
};
这里就出现问题了,teacher 对象必须在 student 对象之前初始化,但是 student 构造函数中使用的是未初始化的 teacher 对象。
解决办法:利用一个函数,定义并初始化本 static 对象,并返回它的引用。
-
class teacher -
{ -
...... -
string pname(){...}; -
}; -
teacher& teach() -
{ -
static teacher t;//定义一个 local-static 对象 -
return t; -
} -
class student -
{ -
...... -
student(params) -
{ -
string a = t().tname(); -
//使用 t() 函数返回的引用,引用期间,teacher对象被初始化 -
} -
}; -
student& stu() -
{ -
static student s; -
return s; -
}
extern 声明的对象对于 teacher 而言是一个 non-local-static 对象, teacher& 函数内声明的对象对于 teacher 而言是一个 local-static 对象。C++保证,local-static 对象在包含它的函数被调用期间(或者说首次遇到这个对象),会被初始化。也就是说,如果用返回引用的函数访问这个对象,就没有调用未初始化对象的困扰。
条款05:了解C++默默编写并调用哪些函数
在创建类时,如果自己不定义默认构造,拷贝构造(拷贝运算符),析构函数,那么编译器会自动生成这些函数。
-
//拷贝运算符: -
classname& operator=(const classname& cn){......}
拷贝运算符注意事项:
若成员变量中有引用,或者被 const 修饰等等,拷贝运算符不可被调用。
-
class person -
{ -
...... -
const int m_age; -
string& m_name; -
...... -
} -
person p1("lisa", 18); -
person p2("luna", 19); -
p1 = p2;//error! -
//const 值不可以修改,引用的指向不可以修改
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
任何人都应该是天地间独一无二的,很难有理由有两个一摸一样的人。所以对于类中拷贝构造函数,我们应当阻止他们。但若是不声明,编译器也会自动生成拷贝构造函数。
-
class person -
{ -
private: -
person(const person&); -
person& operator=(const person&); -
//参数是不必要写的,毕竟这个函数不会被实现 -
public: -
...... -
};
编译器自动生成的函数都是 public 函数,所以我们将 public 改为 private,就可以防止对象调用拷贝构造。
注:private 只有成员函数和友元函数可以调用。
同时也产生了一个问题,如何防止拷贝在成员函数或友元函数中被调用?
答案是建立一个父类,在父类中定义 private 拷贝函数,子类( person 等等)继承父类。因为子类不可以调用父类的 private 函数:
-
class uncopyable -
{ -
private: -
uncopyable(const uncopyable&); -
uncopyable operator=(const uncopyable&); -
}; -
class person{......};
条款07:给多态基类声明 virtual 析构函数
多态把父类当作一个接口,用以处理子类对象:利用父类指针,指向一个在堆区开辟的子类对象。
-
class person -
{ -
public: -
person(); -
...... -
~person(); -
}; -
class teacher: public person{......}; -
person* p = new teacher(...); -
... -
delete p; -
//在堆区开辟的数据要手动删除
上述代码是有问题的。
我们知道,在普通类继承里,删除子类对象会先调用子类的析构,再调用父类的析构。但在多态里情况有所不同。我们删除的是父类指针,调用的只是父类的析构函数,子类析构不会被调用,也就是说,子类对象没有被删除,而指针却没了。这是局部销毁,会造成资源泄漏等错误。
幸运的是,我们可以通过虚函数来解决这个问题。
在多态里,虚函数可以让子类重写父类的函数,同时在虚函数表中生成一个指针,找到子类重写函数的地址,从而让我们可以通过父类访问子类重写的函数。
-
class person -
{ -
public: -
person(); -
...... -
virtual ~person(); -
}; -
class teacher: public person{......}; -
person* p = new teacher(...); -
... -
delete p; -
//删除 p 的时候调用 virtual ~person(); -
//virtual 找到子类析构函数的地址,导致子类也可以被删除
纯虚函数使得父类更像一个接口,这里不用多说。
注:多态里父类应该至少有一个虚函数(virtual 析构),若不用做多态,则类里不应该有虚函数。
条款08:别让异常逃离析构函数
释义:在析构函数内部处理异常
我们来看以下案例:
-
class person -
{ -
public: -
... -
~animal(){...}; -
//假设执行这个析构函数会抛出一个异常 -
}; -
void test() -
{ -
vector<person> v;//假设容器中存储了几个对象 -
... -
v.clear();//现在清空容器 -
... -
add();//摧毁容器之后,剩余的其他操作 -
... -
}
由于对象属于 person 类,在删除的时候会调用析构函数,而 person 的析构函数会抛出一个异常,且未进行异常处理,所以异常会到 test 函数里。 v 中存储了不止一个对象,所以会同时抛出多个异常到test函数里。
在C++程序中,若是同时存在两个异常,则要么结束程序,要么导致不确定行为。结束程序,剩余的操作就无法完成,这对于程序员来说是一个麻烦。
首先介绍一下异常处理的办法:
-
try -
{...} -
//try 内部写可能产生异常的语句,没有产生异常,则catch语句不执行,产生则一一匹配 -
//catch 用于捕获并处理异常,和 case 有异曲同工之妙 -
catch(...) -
{ -
1、可以使用 abort(); 函数终止程序 -
2、可以吞下这个异常,在 catch 内部做一些处理 -
}
了解如何处理异常之后,我们就可以实现如条款所说,在析构函数内部处理异常:
-
class employee -
{ -
public: -
... -
void furlough(); //这是一个放假函数,不放假则抛出一个异常 -
... -
}; -
class manager -
{ -
public: -
... -
~manager() -
{ -
try -
{ -
ep.furlough(); -
} -
//用于确保工人在劳碌后总是可以放假 -
catch(...) -
{ -
//内部实现 -
} -
} -
... -
private: -
employee ep; -
};
但我们对此还有一个想法:
假设员工对不让放假的处理是抗议,如果员工很在意这份工作且不想007,显然,在辞职的情况下抗议是没有意义的,因为他们已经失去了这份工作了。所以他们需要在失去工作之前,对不让放假进行处理。
也就是说,我们需要对函数运行期间抛出的异常做出反应,以防止影响到后面的操作。对此我们可以在类里添加一个普通函数:
-
class manager -
{ -
... -
void holiday() -
{ -
ep.furlough();//如果没有抛出异常,则析构中 if 不会执行 -
//如果抛出异常,则异常回滚到上级函数中 -
//我们在上级函数中,holiday 函数之后可以定义一个 try catch 语句对异常进行处理 -
fangjia = true; -
} -
... -
~manager()//析构函数处理异常作为二重保证 -
{ -
if(!fangjia)// if fangjia == false -
{ -
try -
{ -
ep.furlough(); -
} -
//用于确保工人在劳碌后总是可以放假 -
catch(...) -
{ -
//内部实现 -
} -
} -
} -
... -
private: -
employee ep; -
bool fangjia;//bool 的默认值依赖编译器,前文有提到过,一定要初始化。令 fangjia = false -
};
条款09:绝不在构造和析构函数中使用虚函数
众所周知,在类的操作中,父类比子类先构造,而子类也比父类先析构(多态也是如此,多态先通过 virtual 找到子类析构,再析构父类),所以在构造父类的时候,子类对象还未进行初始化,在析构父类的时候,子类已经被销毁。
此时,如果父类的构造和析构函数中有 virtual ,则该函数无法找到子类的地址(或者说无视子类,因为子类被销毁/未被初始化),使程序发生不明确的行为。
所以 virtual 函数的调用无法下降至子类,但是子类可以将必要的构造信息向上传递到父类:
-
class teacher{ -
public: -
explicit teacher(int score);//父类的构造 -
void score_record(const int& score) const;//non-virtual 函数 -
...... -
}; -
teacher::teacher(const int& score) -
{ -
...... -
score_record(score);//构造执行记录分数的操作 -
} -
class student: public teacher{ -
public: -
student(pare): -
teacher(get_score(para))//将信息传入父类的构造函数,使其记录一个分数 -
{......} -
...... -
private: -
static int get_score(para);//利用一个 static 函数传递分数的值,static 不会传入未初始化的变量 -
};
条款10:令 operator= 返回一个 reference to *this
释义:让赋值运算符重载版本返回一个自身,以便实现链式编程。
-
class employee{ -
public: -
int m_salary; -
employee(int a)//有参构造,赋工资初值 -
{ -
this->m_salary = a; -
} -
employee& operator=(const employee& ep) -
{ -
this->m_salary = ep.m_salary; -
return *this; -
} -
//返回其本身 -
}; -
employee e1(5000); -
employee e2(50000); -
employee e3(123456); -
e1 = e2 = e3; -
//链式编程
条款11:在 operator= 中处理自我赋值
我们来看一段代码:
-
class person{...}; -
person p; -
p = p;
这是自我赋值,这种操作看起来有点愚蠢,但是并不很难发生。
比如,一个对象可以有很多种别名,客户不经意间让这些别名相等;
或者如之前所说,父类的指针/引用指向子类的对象,也会造成一些自我赋值的问题。
自我赋值往往没有什么意义,还会有不安全性。
-
class student{...}; -
class teacher -
{ -
... -
private: -
student* s; -
}; -
teacher& teacher::operator=(const teacher& teach) -
{ -
if(s != NULL) -
{ -
delete s; -
s = NULL; -
} -
s = new student(*teach.s); -
return *this;//便于链式操作 -
}
上述代码是不安全的。如果 *this 和 teach 是同一个对象,那么客户在删除 *this 的时候,也把 teach 删除了,s 就会指向一个被删除的对象。这是不允许的。
我们提供三种方法以解决这个问题:
1、证同检测:
-
teacher& teacher::operator=(const teacher& teach) -
{ -
if (this == &teach) -
return *this; -
//证同检测 -
if (s != NULL) -
{ -
delete s; -
s = NULL; -
} -
s = new student(*teach.s); -
return *this;//便于链式操作 -
}
遗憾的是,证同检测可以保证自我赋值的安全性,但是不能保证“异常安全性”。即,如果 new student 抛出异常,则 s 就会指向一个被删除的对象,这是一个有害指针,我们无法删除,甚至无法安全读取它。
2、记住原指针:
-
teacher& teacher::operator=(const teacher& teach) -
{ -
student* stu = s; //记住原指针 -
if(s != NULL) -
{ -
delete s; -
s = NULL; -
} -
s = new student(*teach.s); //如果抛出异常,s 也可以找回原来地址 -
delete stu; //删除指针 -
return *this;//便于链式操作 -
}
3、copy and swap:
-
void swap(const teacher& teach) -
{......} -
teacher& teacher::operator=(const teacher& teach) -
{ -
teacher temp(teach); //拷贝一个副本 -
swap(temp); //将副本和 *this 交换 -
return *this;//便于链式操作 -
}
交换操作不要考虑原本指针内容,可以保证赋值安全性,同时也能保证异常安全性。
条款12:复制对象时勿忘其每一个成分
释义:自定义拷贝函数时,要把类变量写全(子类拷贝不要遗漏父类的变量)。
父类变量通常存储在 private 里,子类不能访问父类 private 对象,所以应该调用父类的构造函数:
-
class animal -
{ -
public: -
animal(const animal& an) -
{......} -
animal& opeartor=(const animal& an) -
{......} -
...... -
private: -
string typename; -
}; -
class cat: public animal -
{ -
public: -
cat(const cat& c); -
cat& operator=(const cat& c); -
private: -
string cat_type; -
}; -
cat::cat(const cat& c) -
:cat_type(c.cat_type), -
//为了不遗漏父类变量,调用父类函数 -
animal(c) -
{} -
cat::cat& operator=(const cat& c) -
{ -
//为了不遗漏父类变量,调用父类函数 -
animal::operator=(c); -
this->cat_type = c.cat_type; -
return *this; -
}
值得注意的是,上面代码 copy函数和 "="运算符调用的都是和本身一样的函数。究其原因,copy函数是创建一个新的对象,operator= 是对已经初始化的对象进行操作。
我们不能用 copy调用operator=, 以为这相当于用构造函数初始化一个新对象(父类尚未构造好)
同理,也不能用 operator= 调用 copy, 这相当于构造一个已经存在的对象(父类已经存在了)
条款13:以对象管理资源
所谓资源,就是不再使用它时,将其还给系统。
周所周知,堆区开辟的数据需要程序员手动释放,否则会在程序结束的时候由系统释放。在此前提下,我们来看一段代码:
-
class employee{...}; -
void func() -
{ -
employee* emp = new employee();//动态分配一个对象 -
... -
delete emp;//手动释放,否则emp跑出 func() 函数,造成资源泄露 -
}
可以预见,如果在 delete 之前,执行了诸如 return, 抛出异常等等,会导致程序跳过 delete ,使 emp 在堆区开辟的对象未被手动释放,造成资源泄露。(删除的是指针,指针指向的数据没有被删除。)
因为在子函数结束时,其中的类对象会发生析构,所以,我们需要建立一个资源管理类,来防止上述情况的发生。
-
class employee{...}; -
class manager -
{ -
... -
private: -
public: -
employee* empPrt; -
... -
manager(employee* emp): -
empPrt(emp) -
{} -
~manager() -
{ -
... -
delete empPrt; -
} -
}; -
employee* createmp() -
{...}//在堆区创建一个对象 -
void func() -
{ -
manager m(createmp()); -
... -
}
利用 manager 资源管理类创建员工对象,在 func 函数结束的时候,manager析构释放了员工对象总结:建立资源管理类—>管理类存储资源的地址变量—>管理类的构造函数为变量初始化,析构函数手动释放变量在堆区开辟的内存。
条款14:在资源管理类中小心 copying 行为
有的时候,对于资源管理类的 copy 是不必要的,比如管理员工类,每个员工都是单独的个体。但在另一些情况下,copy 也是被程序员需要的。
我们对资源管理类中的 copy 有三种常用的处理方式:
1、禁止 copy 行为:当我们不想某个对象被复制时
-
class manager -
{ -
private: -
//将copy和operator=设为私有 -
... -
};
2、深拷贝:当我们想要多个这个对象的复件时
所谓深拷贝就是在堆区重新开辟一个地址,存储和这个对象相同的数据
(相信同学们都熟练掌握了深拷贝)
3、深拷贝之后删除原件:当我们只想要一个这个对象时
补充一点:引用计数法:
创建一个对象时,其引用数+1,这个对象指向一个新的对象时,其引用数-1,被指向对象引用数+1,当引用数为 0 的时候,delete 这个对象。
条款15:在资源管理类中提供对原始资源的访问
管理类存放的是资源的指针,我们无法从管理类直接得到一个资源对象(只能得到一个指针,通过指针找到对象)。所以我们最好用显式转化或者隐式转换(自动类型转换)来得到一个资源对象:
-
class employee{...}; -
class manager -
{ -
... -
private: -
public: -
employee* e; -
... -
employee get() const -
{ -
return *e; -
} -
//这是显示转化 -
operator employee() const -
{ -
return *e; -
} -
... -
}; -
manager m(...); -
employee emp = m.get();//调用显式 -
employee m1 = m; -
//隐式,manager 对象转换成了 employee 对象
条款16:成对使用 new 和 delete 时要采取相同形式
众所周知,数组名表示数组第一个地址
-
string* s = new char[20]; -
delete s;
所以执行 delete 时,删除的只是第一个对象,后面19个往往不大可能被删除。
对堆区数组的释放可以使用以下方式:
-
string* s = new char[20]; -
delete[] s;
总结:
new <=> delete
new [] <=> delete[]
条款17:以独立语句将 newed 对象置入智能指针
首先介绍一下什么是智能指针:
C++提供智能指针来方便客户对资源进行管理,相当于一个资源管理类。常见的智能指针有:
tr1::shared_ptr<>
auto_ptr<>
它是一个格式像容器的变量类型:
-
//举例: -
tr1::shared_ptr<employee> m(createmp()); -
manager m(createmp()); -
//这两个效果差不多 -
//manager 是自定义的一个资源管理类
以上两个智能指针的主要区别在于 copy 行为上:
tr1::shared_ptr<>在拷贝上允许深拷贝
auto_ptr<>在拷贝上允许拷贝之后删除原件
我们来看一段代码:
-
int func();//这是一个普通的函数 -
//创建一个函数,调用智能指针 -
useemployee(tr1::shared_ptr<employee> (new employee), func()); -
//tr1::shared_ptr<employee> (new employee)语句的执行顺序: -
//先执行 new employee -
//再将 new 的地址存放到 shared_ptr 中
C++对于函数参数的运算顺序有很大的弹性,在其他语言中,是先执行tr1::shared_ptr<employee> (new employee),再执行 func()。
但 C++ 不是,func() 函数可能插在 tr1::shared_ptr<employee> (new employee) 中:
-
//其他语言 -
new employee -
tr1::shared_ptr -
func() -
//C++ -
new employee -
func() -
tr1::shared_ptr
这时候,如果 func() 抛出异常之类,则 new 的地址就无法置入 shared_ptr 中,造成资源泄露。
所以,我们需要一条独立语句将 new 置入 tr1::shared_ptr 中:
-
tr1::shared_ptr<employee> emp(new employee);//独立语句存放地址 -
useemployee(emp, func());//调用
条款18:让接口容易被正确使用,不易被误用
1、保证参数一致性:
-
void print_date(int year, int month, int day) -
{......} -
print_date(2022, 28, 9);//1 -
print_date(2022, 9, 28);//2
在这样一个打印时间的函数接口中,我们按照年月日的顺序输出,但是1式却输出年日月。错误的参数传递顺序造成了接口的误用。
解决办法:
-
class day{...}; -
class month{...}; -
class year{...}; -
void pd(const year& y, const month& m, const day& d){...}
当然,传递某个有返回值的函数也是可以解决的,但这种方法看起来很奇怪。
2、保证接口行为一致性:
内置数据类型(ints, double...)可以进行加减乘除的操作,STL中不同容器也有相同函数(比如size,都是返回其有多少对象),所以,尽量保证用户自定义接口的行为一致性。
3、如果一个接口必须有什么操作,那么在它外面套一个新类型:
employee* createmp();//其创建的堆对象要求用户必须删除
如果用户忘记使用资源管理类,就有错误使用这个接口的可能,所以必须先下手为强,直接将 createmp() 返回一个资源管理对象,比如智能指针share_ptr 等等:
tr1::share_ptr<employee> createmp();
如此就避免了误用的可能性。
4、有些接口可以定制删除器,就像 STL 容器可以自定义排序,比较函数一样
-
tr1::share_ptr<employee> p(0, my_delete());//error! 0 不是指针 -
tr1::share_ptr<employee> p(static_cast<employee*>(0), my_delete());//定义一个 null 指针
第一个参数是被管理的指针,第二个是自定义删除器。
条款19:设计 class 犹如设计 type
对于每一个 class 都要精心设计,要考虑其构造析构函数,初始化和赋值,继承,类型转换,运算符重载,值传递等问题。
条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
释义:用 const引用传递替换值传递
值传递是新创建一个对象,将这个对象和原对象相等,如果用在类里面,当类中成员变量数目较少的时候,也许问题不大(在类里值传递先调用构造,再调用析构)。但当类中成员变量数目过大时,每一次值传递就会造成时间浪费。
引用传递是生成一个别名指向这个地址,其本身是个指针,无论原对象有多少个成员变量,都能在一瞬间找到某一个。用上const令其成为常量指针,即“只读”。
-
class Number -
{ -
public: -
int m_a; -
... -
int m_n;//有 n 个变量 -
... -
}; -
void print1_num(Number num); -
void print1_num(const Number& num); -
Number num1; -
print1_num(num1);//构造,析构一个Number对象 -
print2_num(num1);//传地址
此外,值传递在类里还有一个问题:容易造成切割问题。
比如一个子类继续父类,传递子类对象的时候,可能只创建了一个父类的对象,子类部分缺失了:
-
class base_class -
{ -
virtual void func() const; -
... -
}; -
class derived_class -
{ -
virtual void func() const; -
... -
}; -
void print_class(base_class b);//这是一个打印函数 -
derived_class d; -
print_class(d);
当我们把 d 传入后,参数 b 被构造成了一个父类对象,调用 virtual 函数的时候不会调用子类函数。但我们传入的是子类对象。
条款21:必须返回对象时,别妄想返回其 reference
-
class number{ -
public: -
number(int a); -
const number operator+(const number& n1, const number& n2); //创建一个新对象,返回它 -
//const number& operator+(const number& n1, const number& n2); -
private: -
int m_value; -
}; -
number n1(1);//n1 = 1 -
number n2(2);//n2 = 2 -
number n3 = n1 + n2;
周所周知,return 返回的是一个浅拷贝副本,返回一个对象是没有问题的,但如果返回一个引用,原对象被销毁之后,引用的指向也被销毁了,也就是引用指空,出错。
我们当然可以用 new 解决这个问题,但是当变量数目多的时候,程序员往往不知道怎么使用 delete:
n3 = n1 + n2 + n4 + ...
或许有人想到创建一个 static 对象,但这也是有问题的,我们每次调用都是对同一个 static 操作:
-
const number& operator+(...){static number result;...;return result;} -
bool operator==(const number& n1, const number& n2); -
number n1, n2, n3, n4; -
(n1 + n2) == (n3 + n4);//true
上述对result 进行了两次操作,第一次 n1+n2, 第二次 n3+n4
注:这不同于链式编程,在上述中,我们并不想改变 n1 或者 n2 的值,否则我们直接返回 n1 或者 n2 的引用就可以了。
总结:虽然返回一个对象需要构造,析构等操作而产生一些代价,但是如果我们不想改变已有的值,就最好不要返回一个引用,而是支付这些代价。(这在时间上会多一点,但创建的对象会随运算符的结束而被销毁。这比“未定义行为(返回一个新建对象的引用)”,“资源泄漏”,“结果出错”要好得多了。)
条款22:将成员变量声明为 private
public:所有都能访问
protected:类对象不可以访问
private:只有类成员函数可以访问
private 优点:
1、使成员变量的控制更为精准:
-
class person -
{ -
public: -
void setage(...); -
void setname(...); -
void setid(...); -
private: -
int m_age; -
string m_name; -
int m_id; -
};
用户通过某一函数控制一个私有变量,防止被误用。
2、使类更有封装性:
让我们看看不封装是什么后果:
-
void func1() -
{ -
person p;//创建一个 person 对象 -
p.m_id;//调用其中一个变量 -
func2();//这个函数也调用了 p.m_id -
}
咋一看好像没什么,但如果 func2() 嵌套一个和自己同类型的函数呢?(可以看成递归)
那么 p.m_id 就被无限调用,当代码出错想要修改的时候,想想你的头发,那真是一个灾难。
而封装起来,修改只要改一小部分代码就可以了。
protected不比private有封装性,因为protected子类也可以实现上述代码的操作。
条款23:宁以 non-member, non-friend 替换 member 函数
释义:如果一个成员函数调用了其他的成员函数,那么就要用一个非成员函数替换这个成员函数。
根据条款22,对类变量的操作只能通过类成员函数实现(因为它是私有变量),那么如果一个成员函数内部实现是调用其他的成员函数,则一个非成员函数也可以做到这样的效果:
-
class preson -
{ -
public: -
void func1(); -
void func2(); -
void func3() -
{ -
func1(); -
func2(); -
} -
}; -
void use_all(const person& p) -
{ -
p.func1(); -
p.func2(); -
}
func3() 和 use_all() 的效果是一样的,但这时候我们倾向于选择 use_all 函数,因为func3()作为一个成员函数,其本身也是个可以访问私有变量的函数。use_all() 函数其本身不可以访问私有变量。所以 use_all() 比 func3() 更有封装性。(能够访问私有变量的函数越少越好)
在了解这点之后,我们做一些更深层次的探讨:
我们称 use_func()(func3()的非成员函数版本)为便利函数。假设一个类有多个诸如 func1() 的函数,根据排列组合,也就有很多便利函数。为了让这些便利函数和它的类看上去更像一个整体,我们把便利函数和类放在一个 namespace 中。于是,我们可以更为轻松地拓展这些便利函数——多做一些排列组合。
总结:若一个成员函数调用其他成员函数,那么这个成员函数的非成员函数版本比之拥有更多的封装性,和机能扩充性。
条款24:若所有参数皆需类型转换,请为此采用 non-member 函数
举例:有理数类和整数的运算
-
class Rational -
{ -
public: -
Rational(int numerator = 0, int denominator = 1)//分子与分母 -
... -
const Rational operator*(const Rational& right_number) const; -
... -
}; -
Rational oneEighth(1, 8); -
Rational onehalf(1, 2); -
Rational result1 = onehalf * oneEighth; -
Rational result2 = onehalf * 2; -
Rational result3 = 2 * onehalf;//error!
onehalf*2 相当于 onehaf.operator*(2)
首先创建了一个临时对象 const Rational temp(2);
再让两个 Rational 对象运算。
2*onehalf 是 2 调用了operator*。因为 2 是需要被转换的参数,而 2 的位置和 this(调用operator *) 对象的位置是一样的,所以无法将 2 转换成 Rational 类型,也就无法调用 operator* 函数。
解决办法:使用 non-member 函数,让左右参数的地位平等:
-
const Rational operator*(const Rational& left_number, const Rational& right_number) -
{...}
总结:如果所有参数(运算符左边或者右边的参数)都需要类型转换,用 non-member 函数。
条款25:考虑写一个不抛异常的 swap 函数
周所周知,swap 可以交换两个数的值,标准库的 swap 函数是通过拷贝完成这种运算的。想想,如果是交换两个类对象的值,如果类中变量的个数很少,那么 swap 是有一定效率的,但如果变量个数很多呢?
你一定联想到了之前提过的,引用传递替换值传递。没错,交换两个类对象的地址就可以很有效率地完成大量变量的 swap 操作。不幸的是,标准库的 swap 并无交换对象地址的行为,所以我们需要自己写 swap 函数。
-
class person{...}; -
void my_swap(person& p1, person& p2) -
{ -
swap(p1.ptr, p2.ptr); -
}
这个函数无法通过编译,因为类变量是 private,无法通过对象访问。所以要把它变成成员函数。
-
class person -
{ -
public: -
void my_swap(person& p) -
{ -
swap(this->ptr, p.ptr); -
} -
... -
};
如果你觉得 p1.my_swap(p2) 的调用形式太low了,你可以设计一个non-member 函数(如果是在同一个命名空间那就再好不过了),实现swap(p1, p2),这里不做演示。你还可以特化 std 里的 swap 函数:
-
namespace std -
{ -
template<> -
void swap<person> (person& p1, person& p2) -
{ -
p1.my_swap(p2); -
} -
}
值得注意的是,如果你设计的是类模板,而尝试对swap特化,那么会在 std 里发生重载,这是不允许的,因为用户可以特化 std 的模板,但不可以添加新的东西到 std 里。
还有一点:在上面工作全部完成后,如果想使用 swap ,请确定包含一个 using 声明式,一边让 std::swap 可见,然后直接使用 swap。
-
template<class T> -
void do_something(T& a, T& b) -
{ -
using std::swap; -
... -
swap(a, b); -
... -
}
其中过程:
如果T在其命名空间有专属的 swap,则调用,否则调用 std 的swap。
如果在 std 有特化的 swap,则调用,否则调用一般的 swap。(也即是拷贝)
\\这一点虽然看着很奇怪......
总结:
1、当 std::swap 效率不高时,考虑自定义一个成员函数 swap
2、为成员函数提供非成员函数版本
3、类模板不要特化 swap,类特化 swap
\\4、使用 swap 前要写 std::swap,以便在更多的语境下使用
c++
发布于2022-09-24著作权归作者所有
文章详细介绍了C++编程中的一些最佳实践,包括理解C++的组件,使用const、enum、inline替换#define,确保对象初始化,使用const成员函数,避免异常逃离析构函数,以及在构造和析构函数中处理多态和资源管理。文章强调了良好的编程习惯,如使用const正确指定对象的不变性,使用inline函数提高效率,以及在析构函数中处理异常,确保资源被正确释放。此外,文章还讨论了对象拷贝、赋值运算符的正确实现,以及如何管理堆内存,避免资源泄露。
3832

被折叠的 条评论
为什么被折叠?



