文章目录
程序构建
设计一个badstring类,它将包含一个字符串指针和一个表示字符串长度的值。
badstring.h
#include <iostream>
#ifndef BADSTRING_H_
#define BADSTRING_H_
class badstring
{
private:
char *str;
int len;
static int num_strings;
public:
badstring(const char *s);
badstring();
~badstring();
friend std::ostream &operator<<(std::ostream &os, const badstring &st);
};
#endif
-
char *str
使用的是字符串指针而不是char数组来表示string的内容,这意味着类申明没有为字符串本身分配内存空间。 - num_strings确保每创建一个新对象,共享变量的值都+1,从而记录badstring对象的总数。 另外析构函数将共享变量的值-1。
badstring.cpp
#include <cstring>
#include "badstring.h"
using std::cout;
int badstring ::num_strings = 0;
badstring::badstring(const char *s)
{
int len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
cout << num_strings << ": \"" << str << "\""
<< " object create\n";
}
badstring::badstring()
{
int len = 4;
str = new char[4];
std::strcpy(str, "c++");
num_strings++;
cout << num_strings << ": \"" << str << "\"" << str << " default object create\n";
}
badstring::~badstring()
{
cout << str << " object deleted ,";
--num_strings;
cout << num_strings << " words left\n";
delete[] str;
}
std::ostream &operator<<(std::ostream &os, const badstring &st)
{
os << st.str;
return os;
}
上面将静态成员 num_strings
的值初始化为0。
不能在类声明时初始化静态成员变量,因为声明只是描述了如何分配内存,但并没有分配内存。
main.cpp
#include <iostream>
using std::cout;
#include "badstring.h"
void callme(badstring &st1);
void callme2(badstring st2);
int main()
{
using std::endl;
{
cout << "Start an inner block" << endl;
badstring s1("I am string 1");
badstring s2("I am strnig 2");
badstring s3("I am string 3");
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
cout << "s3:" << s3 << endl;
callme(s1);
cout << "s1:" << s1 << endl;
//按值传递的对象的函数,将调用复制构造函数。
callme2(s2);
cout << "s2:" << s2 << endl;
cout << "initalize one object to another\n";
badstring s4 = s3;
cout << "s4 : " << s4 << endl;
cout << "Assign one object to another :" << endl;
badstring s5;
s5 = s1;
cout << "s5: " << s5 << endl;
cout << "exiting the block..." << endl;
}
return 0;
}
void callme(badstring &str1)
{
cout << "String passed by reference: ";
cout << " \"" << str1 << " \"" << std::endl;
}
void callme2(badstring sb)
{
cout << "String passed by value:\n";
cout << " \"" << sb << " \"" << std::endl;
}
编译和运行结果:
✘ wingchi ~/Cpp/cppPrimer g++ -c vegnews.cpp badstring.cpp
wingchi@wingchi01 ~/Cpp/cppPrimer g++ -c vegnews.cpp badstring.cpp
wingchi@wingchi01 ~/Cpp/cppPrimer g++ badstring.o vegnews.o -o main
wingchi@wingchi01 ~/Cpp/cppPrimer ./main
Start an inner block
1: "I am string 1" object create
2: "I am strnig 2" object create
3: "I am string 3" object create
s1:I am string 1
s2:I am strnig 2
s3:I am string 3
String passed by reference: "I am string 1 "
s1:I am string 1
String passed by value:
"I am strnig 2 "
I am strnig 2 object deleted ,2 words left
s2:
initalize one object to another
s4 : I am string 3
Assign one object to another :
3: "c++"c++ default object create
s5: I am string 1
exiting the block...
I am string 1 object deleted ,2 words left
I am string 3 object deleted ,1 words left
����U object deleted ,0 words left
free(): double free detected in tcache 2
[1] 5742 abort (core dumped) ./main
可以看到程序执行到最后崩溃了,并且在执行过程中就出现了匪夷所思的结果。
程序说明
从创建s1~s3并打印都是没有问题的,
接着调用了callme和callme2函数,前者按引用传递,后者按值传递。 接下来我们出现意外的第一个结果:
String passed by value:
"I am strnig 2 "
I am strnig 2 object deleted ,2 words left
s2:
string2 调用了callme2函数后,就被撤销了(执行了析构函数)
接下来,我们创建了s4,s5字符串,分别以这样的方式初始化:
badstring s4 = s3;
badstring s5;
s5 = s1;
最后,我们结束函数,各个对象分别调用自己的析构函数,结果如下:
exiting the block...
I am string 1 object deleted ,2 words left
I am string 3 object deleted ,1 words left
����U object deleted ,0 words left
free(): double free detected in tcache 2
由于自动存储对象的删除和创建顺序相反,所以最先删除的是:s5(s1),s4(s3),s3
可以看到删除s5和s4都是正常的,删除s3时,就出现了异常。
结果分析
让我们从最先出现奇怪结果的代码进行分析:
callme2(s2);
//String passed by value:
// "I am strnig 2 "
//I am strnig 2 object deleted ,2 words left
熟悉C语言的人应该知道,C语言中的参数,无论是传递值,还是传递地址(指针),都使用了拷贝机制。 (就是将实参拷贝一份给传给函数。)
要解释这个现象,我们首先要了解C++的 复制构造函数.
复制构造函数用于将一个对象复制到新创建的对象中。如果类中没有定义这个函数,将由编译器自动生成. 就是默认复制构造函数. 它用于初始化的过程。 原型如下:Class_name (const Class_name &)
badstring (const badstring &)
对于复制构造函数,要知道两点:什么时候调用和有什么作用:
何时调用:
新建一个对象并将其初始化成一个同类型的现有对象时,复制构造函数都会被调用:
假设已经有一个对象:badstring s0(“I am the original string”); 下面四种声明都有可能调用复制构造函数:
badstring s1(s0);
badstring s2 = s0 ;
badstring s3 = badstring(s0) ;
badstring *ps4 = new badstring(s0);
中间两种声明可能会使用复制构造函数创建s2和s3,也可能使用一个临时对象,然后将临时对象的内容赋值给s2和s3
最后一种声明使用s0初始化一个匿名对象,并将新对象的地址赋值给pstring指针。
每当程序产生了对象副本时,编译器都将使用复制构造函数。 具体地说,当函数按值传递对象或函数返回对象时,都使用复制构造函数。
默认复制构造函数的功能
默认的复制构造函数逐个复制非静态成员,复制的是成员的值(又称浅复制),静态变量不受影响,因为它们属于整个类。
到这里,我们就知道上面调用callme2函数会导致新创建一个对象。函数退出时,这个临时对象会被销毁,也就触发了析构函数。
同时,因为默认复制构造函数是浅拷贝,它在进行复制s2的str变量时,假设有这样一个过程:
temp.str = s2.str ;
这里复制的不是字符串,而是复制指向该字符串的指针,也就是说,临时对象的str成员和s2的str成员保存了指向同一个地址的指针。 当析构函数被调用时,它释放了临时变量的内存,也就释放了s2.str的内存.
说明:默认复制构造函数不说明其行为,因此它不打印创建语句,也不增加技术器strings_num的值。 但析构函数更新了计数,因为任何对象都会被调用。
cout << "initalize one object to another\n";
badstring s4 = s3;
注意上面的赋值运算,也在我们提及的4种声明之一,也就是说:这里也使用默认复制构造函数创建了一个s4对象.
记住,这种初始化语句相当于下面的语句:
badstring s4 = badstring(s3);
cout << "Assign one object to another :" << endl;
badstring s5;
s5 = s1;
//Assign one object to another :
//3: "c++"c++ default object create
可以看到上面的赋值语句导致默认的构造函数被调用了,这里又是什么原因呢? 我们接下来学习另一个概念: 赋值运算符.
赋值运算符
C++允许类对象赋值,这是通过自动为类重载复制运算符实现的,这种运算符的原型是:
Class_name &Class_name::operator=(const Class_name &)
它接受并返回一个指向类对象的引用,例如,badstring类的赋值运算符如下:
badstring& badstring::operator=(const badstring &)
赋值运算符的使用:
badstring s1 ("haha I am a string") ;
badstring s2 ;
s2 = s1 ; // assignment operator invoke
初始化对象时,不一定会使用赋值运算符
badstring s3 =s1 ; // 使用复制构造函数,也可能赋值运算符
和默认的复制构造函数一样,逐个复制非静态成员,(成员复制也称浅复制),复制的是成员的值。静态成员则不受影响,因为他们属于类。