正确的初始化对象
对于内置数据类型,系统不会去初始化, 因为这会增加运行成本.
所以, 永远在使用内置类型前将它们手动初始化;
对于内置类型以外的东西, 系统会自动调用构造函数来初始化。
我们要确保每个构造函数都将对象的每一个成员做了初始化.
初始化对象的成员时,必须分清初始化和赋值的区别,防止二次初始化。
using std::string;
class Player{
Player(string &s, int x, int y);
private:
string _name;
int _x;
int _y;
}
错误的示例1:
Player::Player(string &s, int x, int y){
_name = s;//这是赋值,而不是初始化,string已经被初始化过了。
_x = x;
_y = y;
}
正确的示例2,使用成员初值表 :
Player::Player(string
&s, int x, int y) :_name(s), _x(x), _y(y){
}
为了避免遗漏,必须在初值表中列出所有成员。
错误的示例3:
Player::Player(string
&s, int a) :_name(s), _y(a), _x(_y){
}
C++有着固定的成员初始化次序:
基类的初始化在派生类之前, 成员变量按照声明顺序被初始化(而不是初值表里的顺序).
示例3原意是用a来初始化_y, 再用_y来初始化_x, 实际结果确导致_x是一个未知数。
成员初值表里的顺序必须和声明的顺序相同.
有争议的示例4:
Player::Player(string
&s, int x, int y) :_name(s){
_x = x;
_y = y;
}
这样的编码方式带来了一些隐患,比如成员被遗漏,但也带来了某些好处:
由于内置类型不会被自动初始化,所以它们在成员初值表和构造函数体内进行初始化, 在性能和结果上都是一样的。
对于有多个构造函数的类, 每个构造函数都应该有自己的成员初值表.
这会导致不受欢迎的重复. 如果我们把初始化内置数据类型成员的部分放到一个通用的函数内,就可以消除部分重复代码。
什么时候需要一个虚析构函数
class Base{
public:
Base(){printf("Base constructor\n");}
~Base(){printf("Base distructor\n");}
};
class Derived: public Base{
public:
Derived(){printf("Derived constructor\n";}
~Derived(){printf("Derived distructor\n";}
};
可以看出,Base没有虚函数,不具有多态性质。
如果写出这样的代码:
Base*
pBase = new Derived();
delete pBase;
运行结果如下:
Base constructor
Derived constructor
Base distructor
可以看到, Derived的析构函数没有被执行,在实际运用这可能导致很严重的问题。比如内存泄漏
如果基类不具有多态性质,就不能进行多态编码。
如果我们希望基类带多态性质, 或者类中至少有一个虚函数, 就应该申明虚析构函数.
但也不要无端的将所有类的析构函数都声明为虚, 因为这会增加对象的体积, 并降低代码的可移植性。
绝不在构造函数和析构函数中调用虚函数
class Base{
public:
Base(){Fun();}
virtual ~Base(){Fun();}
virtual Fun(){printf("Base Fun\n");}
};
class Derived: public Base{
public:
Derived(){}
virtual ~Derived(){}
virtual Fun(){printf("Derived Fun\n");}
};
Derived *p = new Derived();
delete p;
运行结果如下:
Base Fun
Base Fun
可见, 不管是构造还是析构,都没有实现多态。原因是:
要实例化 Derived , 首先会调用 Base 的构造函数, 遇到 Fun, 由于 Fun是虚函数,
应该下降调用 Derived 的 Fun函数, 但是Derived 此时还没有被实例化.
实际上只能调用的是 Base 的Fun 函数.
析构的过程是一样的。
程序员应该避免这种不合理的设计出现. 不要再构造函数和析构函数中调用虚函数。
在这两个过程中,多态无法实现。
而且, 构造函数和析构函数调用的函数里也不能嵌套 virtual 函数.
防止隐式调用构造函数带来的意外
如果构造函数只有一个参数, 或者有多个参数但除第一个参数外其余参数都有默认值, 那么这个构造函数承担了两个角色:
1 是个构造器 ,
2 是个默认且隐含的类型转换操作符。
所以, 有时候在我们写下如 A = X, 这样的代码, 且恰好X的类型正好是A单参数构造器的参数类型, 这时候编译器就自动调用这个构造器,
创建一个A的对象。
这样看起来好象很酷, 很方便。 但在某些情况下, 却违背了我们的本意。
这时候就要在这个构造器前面加上explicit修饰, 指定这个构造器只能被明确的调用,不能作为类型转换操作符被隐含的使用。
class Test1{
public:
Test1(int n):_num(n){}//普通构造函数
private:
int _num;
};
class Test2{
public:
explicit Test2(int n):
_num(n){}//explicit(显式)构造函数
private:
int _num;
};
Test1 t1=6;//隐式调用其构造函数,成功
Test2 t2= 6;//编译错误,不能隐式调用其构造函数
Test2 t2(6);//显式调用成功
了解编译器可能自动生成那些成员函数
如果你写下:
class Empty{};
编译器会自动为你写下这些代码:
class Empty {
public:
Empty(){...} // 构造函数
Empty(const Empty& rhs){...} // 拷贝构造函数
Empty& operator=(const Empty& rhs){...} // 赋值函数
~Empty(){...} // 析构函数
};
如果你自己声明了某个函数, 编译器就不再默认创建它
区分拷贝构造函数和赋值函数,并正确的实现它们
拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?
String a(“hello”);
String b(“world”);
String c = a; // 调用了拷贝构造函数,最好写成 c(a);
c = b; // 调用了赋值函数
本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。
倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。
以类String 的两个对象a,b 为例,假设a.m_data 的内容为“hello”,b.m_data 的内容为“world”。
现将a 赋给b,缺省赋值函数的“位拷贝”意味着执行b.m_data = a.m_data。这将造成三个错误:
一是b.m_data 原有的内存没被释放,造成内存泄露;
二是b.m_data 和a.m_data 指向同一块内存,a 或b 任何一方变动都会影响另一方;
三是在对象被析构时,m_data 被释放了两次。
正确的实现类String 的拷贝构造函数与赋值函数
// 拷贝构造函数
String::String(const String &other){
// 允许操作other 的私有成员m_data
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
// 赋值函数
String & String::operator =(const String &other){
// (1) 检查自赋值
if(this == &other)
return *this;
// (2) 释放原有的内存资源
delete [] m_data;
// (3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
// (4)返回本对象的引用
return *this;
}
类String 的赋值函数比构造函数复杂得多。因为它要检查自我赋值。
你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!
这可不好说。而且,间接的自我赋值很有可能出现,例如
a[i] = a[j]; //i,j可能相等
*px = *py;//两个指针指向同一个地址
自我赋值会导致严重的问题。看看第二步的delete,自杀后还能复制自己吗?
所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if 语句
if(this == &other)
错写成为
if( *this == other)
注意函数strlen 返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy 则连‘\0’一起复制。
返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。
如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?
只需将拷贝构造函数和赋值函数声明为私有函数,并不予实现。
更多自我赋值的思考
我们通常用“证同测试”来避免自我赋值,比如这样
String & String::operator =(const String &other){
if(this == &other)
return *this;
delete [] m_data;
m_data = new char[strlen(other.m_data)+1];
strcpy(m_data, other.m_data);
return *this;
}
实际上,自我复制的发生频率很低,这样证同测试就有点影响效率了。
有一个办法既可以避免自我赋值,又不需要证同测试。
String & String::operator =(const String &other){
char* p = m_data;
m_data = new char[strlen(other.m_data)+1];
strcpy(m_data, other.m_data);
delete p;
return *this;
}
还有一个十分巧妙并且更高效的方法:
String & String::operator =(String other){//注意这里是传值
char* p = other.m_data;
other.m_data = m_data;
m_data = p;
return *this;
}
酷!它把数据拷贝动作从函数体内转移到函数参数的构造阶段,可以让编译器生成更高效的代码。
但是它牺牲了清晰性,请酌情使用。
正确理解对象占用的内存
理论上, 空对象不占用内存, 但是系统为了区分不同的空对象, 仍然分配了一小块地址, 通常是1字节.
系统为每个类维护了一个方法表., 所以方法不会占用对象的内存, 如果一个对象只有方法没有变量, 它占用的内存也是1字节.
类只需要为它的每个静态变量维护一个实例, 所以静态变量也不会占用对象的内存.
如果类包含虚函数, 系统需要为它维护一个虚函数表. 为了实现多态, 对象必须保存它所属的类的虚函数表的指针.
所以一个只有虚函数的类, 它的对象占用的内存是一个指针大小, 4字节.
虚函数表是一个在内存上连续的函数指针数组, 函数指针分别指向类中的每一个虚函数.
虽然一个子类的对象地址可以被赋值给一个基类指针, 由于这个对象保存了它真正的类的虚函数表地址, 所以它总能正确的调用自己的虚函数.
for (iterator it=map.begin(); it!=map.end(); it++){
if (it->first == a){
map.erase(it);
}
}
正确的方法, 方法 2:
for (iterator it=map.begin(); it!=map.end(); ){
if (it->first == a){
map.erase(it++);
}else{
it++;
}
}
这样也可以, 方法 3:
for (iterator it=map.begin(); it!=map.end(); it++){
if (it->first == a){
it = map.erase(it);
it--;
}
}
为什么方法2是对的? 方法1和方法2不是一样的吗?
it++操作主要做三件事情:
1、首先把it备份一下。
2、把it加上1。
3、返回第一步备份的it。
因此,map.erase(it++);
在执行erase之前,it已经被加一了。
erase会使得以前那个未被加一的it失效,而加了一之后的新的it是有效的。
这段代码的真正等效代码是
iterator iterTemp = it;
++it;
map.erase(iterTemp);
反之,如果这样:
map.erase(it);
it++;
那么erase操作直接让it失效,对失效的it进行加一操作也是失效的。
删除map中的一个节点后, map会发生什么变化?
map中其它节点的地址不会发生变化, 变化的只是某些节点的左右节点指针.
明白string带来的额外内存开销
观察一块测试代码
string s;
cout<<sizeof(s)<<endl;//28
cout<<s.size()<<","<<s.capacity()<<endl;//0,15
s.resize(8);
cout<<s.size()<<","<<s.capacity()<<endl;//8,15
s.resize(16);
cout<<s.size()<<","<<s.capacity()<<endl;//16,31
s.resize(1000);
cout<<s.size()<<","<<s.capacity()<<endl;//1000,1007
一开始, string 是空的, 它的 size 是 0,
但是在实例化的时候, 它已经预先申请了一块内存备用.
这块内存的大小是 15, 我推测, 应该是 16, 有一个字节被内部使用了.
另外, string 的内部数据占用了 28 个字节, 所以一个空string, 也要使用 44 个字节的内存. 有点心疼呀.....
随着 string 里数据的增加, 申请的内存总是 16 的倍数.