本章节的学习以一个存在诸多bug的类StringBad开始,通过不断的总结该类中出现的问题以及如何解决的来学习类中的动态内存分配方法。
查到一个关于类的各种构造函数详细分析的博文,有兴趣可以参看一下:
http://www.cnblogs.com/xkfz007/archive/2012/05/11/2496447.html
首先是类的声明:
class StringBad
{
private:
char * str; // pointer to string
int len; // length of string
static int num_strings; // number of objects
public:
StringBad(const char * s); // constructor
StringBad(); // default constructor
~StringBad(); // destructor
// friend function
friend std::ostream & operator<<(std::ostream & os,
const StringBad & st);
};
下面是对类的定义:
int StringBad::num_strings = 0;
// construct StringBad from C string
StringBad::StringBad(const char * s)
{
len = std::strlen(s); // set size
str = new char[len + 1]; // allot storage
std::strcpy(str, s); // initialize pointer
num_strings++; // set object count
cout << num_strings << ": \"" << str
<< "\" object created\n"; // For Your Information
}
StringBad::StringBad() // default constructor
{
len = 4;
str = new char[4];
std::strcpy(str, "C++"); // default string
num_strings++;
cout << num_strings << ": \"" << str
<< "\" default object created\n"; // FYI
}
StringBad::~StringBad() // necessary destructor
{
cout << "\"" << str << "\" object deleted, "; // FYI
--num_strings; // required
cout << num_strings << " left\n"; // FYI
delete [] str; // required
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
使用类的例子:
#include <iostream>
using std::cout;
#include "strngbad.h"
void callme1(StringBad &); // pass by reference
void callme2(StringBad); // pass by value
int main()
{
using std::endl;
{
cout << "Starting an inner block.\n";
StringBad headline1("Celery Stalks at Midnight");
StringBad headline2("Lettuce Prey");
StringBad sports("Spinach Leaves Bowl for Dollars");
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
callme1(headline1);
cout << "headline1: " << headline1 << endl;
callme2(headline2); //开始出错
cout << "headline2: " << headline2 << endl;
cout << "Initialize one object to another:\n";
StringBad sailor = sports;
cout << "sailor: " << sailor << endl;
cout << "Assign one object to another:\n";
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n";
}
cout << "End of main()\n";
return 0;
}
void callme1(StringBad & rsb)
{
cout << "String passed by reference:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb)
{
cout << "String passed by value:\n";
cout << " \"" << sb << "\"\n";
}
---------------------------------------------------------------------拆解分析--------------------------------------------------------------
首先类中有一个static类型的成员变量。不能在类声明中对static进行初始化,因为类声明只是描述如何分配内存,但是并没有分配内存。同时类中的静态成员变量并不属于哪个对象所有,而是独立开辟出一块内存来存储。(因此有类似的面试题,求一个类对象的实际大小的时候,需要排除静态成员变量占用的内存空间)。但是如果用const修饰静态成员,并且是int型或者enum类型,则可以在声明的时候对其初始化。
其次,当程序运行到callme2(headline2)时,该函数的参数是按值传递,因此首先导致headline2的析构函数被调用,然后创建一个临时对象用来存储headline2的成员变量。但是由于headline2已经进行了析构,存储成员变量的内存可能已经被修改,因此这里会出错。
接着,在StringBad sailor = sports中,这里使用了构造函数,但不是上面显式定义的两种构造函数,而是复制构造函数(如果没有显式的提供,编译器会提供一个默认的复制构造函数。该赋值复制构造函数中没有对计数器num_strings加1)。sailor消逝时,会调用显式的析构函数,num_strings--。
-------------------------------------------------------------------详细分析-----------------------------------------------------------------
C++中,编译器会自动给一个类提供下面五个成员函数(如果没有显式的定义的话):
1. 默认构造函数
2. 默认析构函数
3. 复制构造函数
4. 赋值运算符
5. 地址运算符
对于默认构造函数,如果显式的提供,有两种方式:无参数的构造函数或者带参数但是参数必须都有默认值的构造函数。
对于复制构造函数,指的是以一个类的对象作为参数的构造函数。复制构造函数的原型如: StringBad str2(const StringBad &)。以下四种情况都会调用复制构造函数:
1. StringBad str(str1); //假设str1是已存在的StringBad对象
2. StringBad str = str1;
3. StringBad str = StringBad(str1);
4. StringBad *str = new StringBad(str1);
注意:按值传递将会创建一个原始对象的副本,会调用复制构造函数。如果构造函数中存在数组或者字符串指针,传递的也将是一个指针,而不是新开辟一块空间来存放数组或者字符串的内容,因此按值传递,如果原始对象消亡了,新对象中数组或者字符串指针所指的区域可能已经不存在原始对象的内容,从而导致错误。
另外,如果类中存在静态变量,并且在新对象创建时对静态变量进行修改,则应该提供一个显式复制构造函数来处理对静态变量的修改。
对于赋值运算符,这种运算符的原型如: Class_name & Class_name::operator=(const Class_name &).它接受并返回一个类对象的引用。赋值构造函数也会产生和复制构造函数一样的潜在错误。解决赋值运算符产生的问题可以显式的构造一个赋值运算符,但是要注意三点:
1. 防止将对象赋给自身;2. 如果存在已分配的空间注意使用delete先释放这些空间;3. 函数返回一个调用对象的引用。
对于地址运算符,将返回调用对象的地址(即this指针的值)
--------------------------------------------其他知识点-----------------------------------------
如果类中存在静态成员函数,则只能使用静态成员变量。因为静态成员函数也是属于类的,而不是属于哪个对象,因此无法调用普通的成员变量(属于某个对象)。
可以存在多个构造函数,但是只能存在一个析构函数(考虑到继承问题,一般将析构函数设置为虚函数virtual)。
如果返回值为const修饰的对象引用,则表示返回的对象是个不能够修改成员变量的对象。(一般使用const引用是为了提高效率)
如果返回的对象是被调函数中的局部变量,则不应该返回一个引用,而是返回一个对象。
如果使用定位new运算符,则不能使用常规的delete运算符与之配合使用。而是要显式的调用析构函数。
---------------------------关于C++类的构造函数的一道考题(出自2013某播放器公司的笔试题)-------------------
如果已声明一个类A,及class A,问A a,*b[2],c[3],&d=a; 共调用了几次构造函数?
我在努力思考中,如果你能坚持看到这里,并能顺利解出该题,麻烦讲解一下。