对于一般的对象,如:
int b = 20;
它们之间的赋值、复制过程是很简单的。但是对于类对象来说,其内部存在各种类型成员变量,在拷贝过程中会出现问题。如下:
2 #include<cstring>
3 using namespace std;
4 class String {
5 public:
6 String (const char* psz=NULL) : m_psz(strcpy(new char[strlen(psz?psz:"")+1]),psz?psz:""){
7 cout << "String构造" << endl;
8 }
9 ~String () {
10 if(m_psz) {
11 delete[] m_psz;
12 m_psz = NULL;
13 }
14 cout << "String析构" << endl;
15 }
16 char* c_str(void) {
17 return m_psz;
18 }
19 private:
20 char* m_psz;
21 };
22 int main(void) {
23 String s1("hello");
24 String s2(s1);
25 cout << "s1 " << s1.c_str() << endl;
26 cout << "s2 " << s2.c_str() << endl;
27 s1.c_str()[0] = 'H';
28 cout << "s1 " << s1.c_str() << endl;
29 cout << "s2 " << s2.c_str() << endl;
30 return 0;
31 }
./a.out
编译通过了,运行后出现一堆的错误,为什么?!这就是浅拷贝带来的问题。
事实是,在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。原型如下:
但凡是编译系统提供的缺省函数,总不是十全十美的。
缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标----浅拷贝。
用下图来解释这个问题:
在进行对象复制后,事实上s1,s2里的成员指针m_psz都指向了一块内存空间(即内存空间共享了),在s1析构时,delete了成员指针m_psz所指向的内存空间,而s2析构时同样指向(此时已变成野指针)并且要释放这片已经被s1析构函数释放的内存空间,这就让同样一片内存空间出现了“double free” ,从而出错。而浅拷贝还存在着一个问题,因为一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了,正如程序中只改变了s1.c_str()[0] = 'H',然而输出的s1,s2均为
hello,所以这并不是真正意义上的复制。
为了实现深拷贝,往往需要自己定义拷贝构造函数,在源代码里,我们加入自定义的拷贝构造函数如下:
cout << "String拷贝构造" << endl;
}
这样再运行就没有问题了。
在程序中,还有哪些情况会用到拷贝构造函数呢?当函数存在对象型的参数或对象型的返回值时都会用到拷贝构造函数。
而拷贝赋值的情况基本上与拷贝复制是一样的。只是拷贝赋值是属于操作符重载问题。例如在主函数若有:String s3;s3 = s2;这样系统在执行时会调用系统提供的缺省的拷贝赋值函数,原型如下:
我们可以自定义拷贝赋值函数如下:
m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);
}
但是这只是新手级别的写法,考虑的问题太少。我们知道对于普通变量来讲a=b返回的是左值a的引用,所以它可以作为左值继续接收其他值(a=b)=30,这样来讲我们操作符重载后返回的应该是类对象的引用(否则返回值将不能作为左值来进行运算),如下:
m_psz = strcp
在C++中当创建一个空类时,C++就会默认的为这个类创建4个函数:默认的构造函数、析构函数、拷贝构造函数、以及赋值操作符。本文参考Effective C++介绍这几个函数。
目录:
1. 函数的原型以及创建函数的时机
2. 赋值操作符存在的问题
3. C++0x的新变化
1. 函数的原型以及函数创建的时机
C++中创建一个空类:
默认会生成4个函数,其函数的原型如下:
Empty() { ... }
Empty(const Empty& rhs) { ... }
~Empty() { ... }
Empty& operator=(const Empty& rhs) { ... }
说明:1) 这些函数只有在需要调用的时候,编译器才会生成。2) 4个函数都是public的。3) 4个函数都是inline的(即函数定义在类的定义中的函数)。4) 如果你显式的声明了这些函数中的任何一个函数,那么编译器将不再生成默认的函数。
比如,当遇到下列语句时,函数会被编译器生成:
//对象销毁时,析构函数
Empty e2(e1); //拷贝构造函数
e2 = e1; //赋值运算符
另外,还存在两种默认的函数:就是取地址运算符和取地址运算符的const版本,这两个函数在《Effective C++》中没有提及。
Empty* operator&() { ... }
const Empty* operator&() const { ... }
这两个函数是确实存在的,正如下面的代码可以正常工作:
class Empty {
};
int main(int argc, char** argv)
{
Empty a;
const Empty *b = &a;
printf("%p\n", &a); //调用取地址运算符
printf("%p\n", b); //调用const取地址运算符
}
一个容易被忽略的问题:自定义的拷贝构造函数不仅会覆盖默认的拷贝构造函数,也会覆盖默认的拷贝构造函数。下面的代码是编译不过的,用户必须再显式的定义一个无参的构造函数。
public:
Empty(const Empty& e) { } //拷贝构造函数
};
int main(int argc, char** argv)
{
Empty a;
}
2. 赋值操作符存在的问题
赋值操作符函数的行为与拷贝构造函数的行为基本是相同的,编译器生成赋值操作符函数是有条件的,如果会产生无法完成的操作,编译器将拒绝产生这一函数。那么什么时候编译器无法完成赋值这一行为呢?考虑如下情形(来源Effective C++):
class NameObject {
public:
NameObject(std::string& name, const T& value);
private:
std::string& nameValue; //引用成员变量
const T objectValue; //const成员变量
};
然后考虑下面的语句会发生什么事:
std::string oldDog("xxx");
NameObject<int> p(newDog, 2);
NameObject<int> s(oldDog, 10);
p = s; //将会发生什么?
赋值语句之前,p.nameValue指向newDog, s.nameValue指向oldDog。那么赋值之后呢?p.nameValue应该指向s.nameValue指向的对象吗?但是C++有一条规定:引用不能改指向另外一个对象。
对于变量objectValue,C++规定:更改const成员是不合法的。
因此如果上面两种情形中的任何一种发生了,C++编译器给出的响应是:拒绝编译这一行的赋值动作。如果你这么做了,C++编译器会报错。如果你执意要进行赋值操作,那么可以自己定义一个赋值操作符重载函数。
3. c++0x中的新变化
C++0x中引入了“右值引用”和“移动语义”的概念,可以实现对右值的引用。(左值和右值的解释可以见http://amyz.itpub.net/post/34151/411832)
移动语义,简单来说,就是在一个右值对象的生命期结束之前,将其资源偷过来,为我所用。有关移动语义的详细内容这里不做详述,大家可以参见csdn上一篇文章 http://blog.csdn.net/pongba/article/details/1684519。这里要说明的是移动构造函数和移动赋值运算符。
1. 移动构造函数和移动赋值运算符重载函数不会隐式声明,必须自己定义。
2. 如果用户自定义了拷贝构造函数或者移动构造函数,那么默认的构造函数将不会隐式定义,如果用户需要,也需要显式的定义。
3. 移动构造函数不会覆盖隐式的拷贝构造函数。
4. 移动赋值运算符重载函数不会覆盖隐式的赋值运算符重载函数。
本文链接
最近在学习并发程序设计,其中有个很重要的概念叫原子操作。网上有很多文章论述原子操作的,其中大部分文章不约而同的都使用到了这个例子——“++”操作,来例证很多高级语言中的一条语句并非是不可拆分的原子操作。出于好奇,本人对“++”操作的原子性在VS2012下写了一个小程序以测试之,于是乎发现了下面的问题。
TEST(ConcurrenceTest, Atomic)
{
std::vector<std::thread> threads;
threads.push_back(std::thread(std::ref(thread)));
threads.push_back(std::thread(std::ref(thread)));
for(auto &t : threads)
{
t.join();
}
std::cout<<"total值:"<<total<<std::endl;
}
//线程
void thread()
{
for(int i = 0; i < 50000; i++)
{
total++;
}
}
以上代码在release下结果都是100000,但在debug下会小于100000。
了解原子操作的朋友应该知道,debug下小于100000的结果应该属正常现象,因为“++”操作并不具有原子性,所有在并发的过程中会出现数据竞跑的现象。但是在release下所得到的结果却总是正确的(为了避免偶然性,本人用更多的线程,更大的数据类型同样做过测试,结果依然正确),这不得不怀疑编译器在release下对代码是否做过一定的优化? 那么这种优化对于程序员来说是一件好事么? 他会不会给一些对此了解不深的程序员造成一种正确的假象?本人是个初学者,写这些的目的只是抛个砖,以上的观点也仅是本人的一些小想法,希望有兴趣的朋友能来一起讨论。
本文链接