序章
关于术语
声明式
extern int x;
函数的声明 即 函数的签名式
void ShowNum(const int x);
定义式
即为其申请内存空间、提供代码本体
- 条款01:视C++为一个语言联邦
可面向过程,相当于C语言
可面向对象,利用类思想,批处理相似的模块
主要的次语言:C语言(基础)、object-oriented C++(面向对象)、Template C++、STL。
讨论: 书中提到对于内置类型而言,值传递比引用传递更高效。
int f(int i)
{
int r = i + 1;(1)
return r;
}
int g(const int & i)
{
int r = i + 1;(1)
return r;
}
汇编角度看,
值传递,只会在(1)将i值赋给eax寄存器,然后eax+1,eax再赋值给r;
引用传递:在(1)将i值给eax,eax值再给ecx寄存器,ecx+1,最后ecx才赋值给r;
一般时引用传递更高效。
原因:若使用值传递,当参数是类对象,
先拷贝一份实参的副本,创建时会调用构造函数,如果该类是派生类,会有很多构造析构函数调用。
使用引用传递,没有构造函数或析构函数被调用,需要const防止对象被修改。
- 条款02:尽量以const、enum、inline替换#define
宁可编译器替换预处理器。
预处理器:例如,词法预处理器是删掉注释,宏展开等操作
讨论:
#define NUM 99
如果运行时,常量错误,错误信息只会显示99,而你不知道是NUM,会产生疑惑,很难找到99的来源。
并且const、enum、inline可作为作用域成员,而#define不能。
若要定义一个常量的char*字符串,要写const两次
提醒:string比char*合宜
const char* const str = "effective c++";
第1个const:常量指针,指向的内容不可改,指针可改;
第2个const:指针常量,指向的内容可改,指针不可改;
讨论:class专属常量,必须是static成员
class A{
private:
static const int Num = 9;//旧的编译器可能不支持,初始化放在定义式,static一定要写定义式
}
原因:确保常量的作用域在类内
- 条款03:尽可能使用const
编译器会强制实施const这项语义约束
讨论: 书中提到
class A {
public:
A():num(2) { }
~A() { }
void setnum() const { num = 10; } //const函数
private:
int num;
};
int main()
{ A b;
b.setnum(); //编译错误
原因:setnum函数被声明为const,,在编译器的编译过程中,就将这个类方法中的隐藏参数this转换成了const this,这样一来,就不能对类成员进行修改。
return 0; }
class A
{
public:
A():num(2)
{}
~A()
{}
void setnum()
{
num = 10;
}
void getnum() const{
printf("%d\\n",num);
}
private:
int num;
};
class B
{
public:
B()
{}
~B()
{}
const A* get()
{
A *p = new A();
return p;
}
};
int main()
{
B b;
b.get()->getnum();
//通过类B的方法get()来获得一个类A的实体const对象
//调用A的getnum方法(const)
b.get()->setnum();//编译错误
//const类型的对象,不能调用自身的非const成员函数
return 0;
}
讨论: const类型的对象,不能调用自身的非const成员函数的原理
class A
{
int a;
public:
void show();//void show(A* this),代表一个指向A对象的指针this被传入到了show函数中
void print() const;//void print(const A* this);
A(int i);
}
c++在类的成员函数中还会隐式传入一个指向当前对象的this指针;
A a(23);
a.show();//相当于show(&a);
const A b(24);
b.show();//此时指向b地址的指针类型是const A* this;所以类型不匹配!
b.print();//const对象可以调用const函数
A c(12);
c.print();
//非const指针赋值给const指针,可以
//所以非const对象是可以调用const成员函数的
- 条款04:确定对象被使用前已先被初始化
array不保证内容被初始化,即不会被默认初始化;
在构造函数中初始化,应该使用成员初始列表;
A(const int i,const string& str)
{
m_i=i;
m_str=str;
]
这是赋值(伪初始化)不是初始化,
string是类函数,它的初始化在default构造函数自动被调用时;
int是内置类型,不保证赋值之前就获得初值;
使用成员初始化列表效率高,列表m_i以i为初始值调用copy构造;
而赋值操作,先default构造函数后又赋值,default构造函数没有任何用处。
- 条款05:了解C++默默编写并调用哪些函数
当你写一个空类,当被调用时,编译器才会自动声明:copy构造函数、构造函数、copy assignment函数、析构函数,都是public且inline。
讨论: 不允许让reference改指向不同对象。
int i=1,j=5;
int& k=i;
k=j;
//仍旧引用i,但i的值是5
&k=j; //才是改变引用
会报错
- 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
条款提出应该不能拷贝对象即:
Home a;
Home c;
Home b(a);//编译应该失败
c = a;//编译应该失败
原本不声明函数就不会调用,但条款5说明copy拷贝函数是自动生成的。
为了阻止调用,可以将copy构造函数设置为private
- 条款07:为多态基类声明virtual析构函数
拓展:工厂模式规定,无论是工厂函数,工厂类的成员函数,返回的对象都必须位于heap。需要delete掉防止内存泄漏。
class Base{
public:
Base() { cout<<"Base Creted"<<endl; }
~Base() { cout<<"Base Destroyed"<<endl; }
};
class Derived: public Base {
public:
Derived() { cout<<"Derived Created"<<endl; }
~Derived() { cout<<"Derived Destroyed"<<endl; }
};
int main()
{
Base *pB = new Derived();
delete pB;
//结果:Base Creted,Derived Created,Base Destroyed
}
pB是基类指针,虽然指向的是派生类,只能调用自己的函数,是无法通过基类指针调用到子类的成员函数的
所以父类析构函数要使用virtual,在父类中通过virtual 修饰析构函数后,通过 父类指针再去指向子类对象,然后通过delete 接父类指针,就可以释放掉子类对象。
讨论: 为何可以解决?
使用virtual时,父类虚函数表会有一个函数指针指向父类析构函数,子类虚函数表也会有一个函数指针指向子类析构函数。当delete父类指针指向的子类时,通过子类对象的 虚函数表指针 找到子类的 虚函数表,再通过子类 的虚函数表找到子类的析构函数,从而使得子类的析构函数得以执行,子类的析构函数执行完毕后, 系统会自动执行父类的析构函数。
vptr即指向虚函数表的指针,vptl即虚函数表的有函数指针构成的数组。vptr会增加对象的体积。
从书中得知,声明virtual析构函数只适用于具有多态的父类上。
- 条款08:别让异常逃离析构函数
class YYY
{
public:
~YYY()
{
uninit();
}
private:
void uninit()
{
...
std::cout << "void YYY::uninit() exception\n\n";
}
};
若析构函数中出现异常时,程序直接终止,析构函数会允许它离开析构函数,异常一层层向上传递,到main终止程序,很麻烦。
解决:
try
{
// 自己手动抛出一个异常。
throw 1;
}
catch (...)
{
// 吞并所有异常。
std::cout << "void YYY::uninit() exception\n\n";
//终止程序
std::abort();
}
- 条款09:绝不在构造和析构过程中调用virtual函数
如书中例子所写,在创建派生类对象时,会先调用基类的构造函数,而构造函数内的虚函数是基类的虚函数。
值得一提的是,若调用纯虚函数会报错警告,但当调用的是有实现代码的虚函数时,是没有报错,会给人带来困扰(可以输出基类的虚函数内的操作而不是派生类的)。
- 条款10:令operator = 返回一个reference * this
新增拓展补充:https://blog.csdn.net/weixin_39905871/article/details/113606907
因为this是指向本对象的指针,那么this就是该对象的本身实体了。而现在返回的只不过是该实体的一个代表符号而已。现在一般的赋值操作符都采用这个原则。
讨论: this和 * this 的qubie
return *this返回的是当前对象的克隆或者本身(若返回类型为A, 则是克隆, 若返回类型为A&, 则是本身 )。
return this返回当前对象的地址(指向当前对象的指针)
class A
{
public:
int x;
A* get()
{
return this;
}
A gets()
{
return *this; //返回当前对象的拷贝
}
};
A a;
if(&a == a.get())
{
cout << "yes" << endl;
}
if(a.x == a.gets().x)
{
cout << a.x << endl;
}
else
{
cout << "no" << endl; //输出no
}
- 条款11:在operator = 中处理“自我赋值”
因为在某些时候要编写用来管理资源的类,以往学习可知一个资源不用了就要释放掉,以便留给下一个需要该资源的对象。但是,假设当前占用该资源的对恰好是用来赋值的右值,也就是它俩其实是一个东西。如果还是按照上面处理的话,就会出现被用来赋值的右值实际上已经啥实质内容都没有了,this指针指向了NULL,那就会发生错误。
讨论: 如何解决?
第一种方法,在赋值操作符的实现中最先判断一下赋值的对象和被赋值的对象是不是一个,即if(this==&obj)。不过这种方法不好,因为这需要额外写出一条语句,这无疑会增加运行时间。
第二种COPY and SWAP方法,首先赋值一份右值,生成一个COPY,然后用*this与这个COPY进行SWAP,那么就可以解决自我赋值的问题。因为既然是COPY那么原来那个右值就没有改变,而this原本是空的,它和那个COPY交换以后,那个COPY就变成了NULL,而this的内容就成了COPY,也就是原来的那个右值的值了。
第三种方法,直接利用赋值操作符重载函数的传参机制是传值这一特性,直接传进来一个COPY。但不提倡,原因应该是可读性(这里有些理解模糊)。
- 条款12:复制对象时勿忘其每一个成分
(1)会忘记拷贝基类成分
一个从基类base继承的派生类derive,类内声明并定义了拷贝构造函数和拷贝赋值函数
class base{
public:
base(const base& rhs):baseVal(rhs.baseVal){}
private:
int Val;
};
class derive: public base{
public:
derive(const derive& rhs):val(rhs.val){...}//修改::base(rhs)
derive& operator=(const derive& rhs){
rhs = rhs.val;//拷贝赋值函数添加base::operator=(rhs);
return *this;
}
private:
int Val;
};
两个拷贝函数对另一个类的复制并不完整,因为只考虑了对派生类成分的拷贝,而忽略了对基类成分的拷贝。
定义类的拷贝函数时,应注意两点:
1.复制所有局部成员变量
2.调用所有基类内的适当的拷贝函数