构造函数与析构函数;形参选择;const关键字;对象数组;运算符重载、友元函数;自定义类型转换;静态成员变量;特殊成员函数

前言

关于面向对象OOP,本文不再做说明。本文的重点是说明:在使用C++这门语言进行程序开发中,对类和对象的使用需要注意的地方。

插一句:结构和类的不同,在C++中表现在于:类的默认成员为private,结构是public——《C++ Primer》

构造函数与析构函数

构造函数与析构函数的调用时间,其实相对容易理解。构造函数在初始化对象的时候会调用到,析构函数在对象被销毁时调用。

关于析构函数被调用的时间,以我们常用的“自动存储对象”,在执行到其作用域之后,析构函数就会被调用。如果是使用new初始化的对象,在delete删除的时候,调用析构函数。

举个例子用于说明临时变量:

类Rectangle的定义:

class Rectangle{
private:
	int height;
	int width;

public:
	Rectangle(int height1, int width1){		//构造函数
		height = height1;
		width = width1;
		std::cout << "init Rectangle instance" << std::endl;
		std::cout << "height:" << height << "  width:" << width << std::endl;
	}

	~Rectangle(){		//析构函数
		std::cout << "Rectangle(" << height << "," << width << ")distroyed" << std::endl;
	}

main函数:

int main(){
	{
		Rectangle rect1 = Rectangle(1, 1);
		Rectangle rect2 = Rectangle(2, 2);
		rect1 = Rectangle(3, 3); //创建了临时变量,并立刻销毁
		Rectangle rect4 = Rectangle(4, 4);
		Rectangle rect5;  //没有默认构造函数,不能编译
	}
}

由于自定义了带参数的构造函数,没有了不带参数的默认构造函数,所以rect5不能编译通过。删除这一行之后,

结果分析:在将Rectangle(3,3)赋值给rect1之后,会马上删除掉临时变量,我们可以看到析构函数被调用了一次。后面析构函数的调用顺序说明了,局部变量是被放在栈中,先实例化的后销毁。

const函数

在说const函数是什么之前,我们设想以下一种情况:

//我们给Rectangle类定义一个成员函数,计算矩形的面积
int Rectangle::area(){
	return width*height;
}

//main函数
const Rectangle rect(1,1);
rect.area();	//编译不通过

当我在编译器中写下这样的代码之后,有个错误提示——rect对象是const类型,不能确保在调用area()时不改变rect。解决方法就是如下声明:

int area() const;

int Rectangle::area() const{
	return width*height;
}

对象数组

我在阅读《C++ Primer》这本书时,看到这么一段:

初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数会创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,这个类必须有默认构造函数。P369

但是在实际应用中,情况并不是这样。

Rectangle rectArr[2] = {
	Rectangle(2, 2),
	Rectangle(3, 3)
};		//没有问题

Rectangle rectArr[3] = {
	Rectangle(2, 2),
	Rectangle(3, 3)
};		//编译错误

重新添加了默认构造函数之后,发现[2]数组声明过程中没有调用默认构造函数,[3]数组声明调用了一次。

运算符重载与友元函数

这里先举两个运算符重载的例子,然后说明为什么需要友元函数。

第一个:

//运算符 +
Rectangle Rectangle::operator+(const Rectangle & rect) const{
	Rectangle result;
	result.height = this->height + rect.height;
	result.width = this->width + rect.width;
	return result;
}

//使用
Rectangle rect1(1, 1);
Rectangle rect2(1, 1);
std::cout << (rect1 + rect2).area()<<std::endl;
//本质rect1.operator+(rect2).area()

第二个:

Rectangle Rectangle::operator*(int arg) const{
	Rectangle result;
	result.height = this->height * arg;
	result.width = this->width * arg;
	return result;
}

//使用情景一
Rectangle rect1(2, 2);
std::cout << (rect1 * 2).area()<<std::endl;
//本质rect1.operator+(2).area()

//使用情景二
Rectangle rect1(2, 2);
std::cout << (2 * rect1).area()<<std::endl;
//由于没有对int进行 * 重载(也不能对基本类型进行重载),不能这么使用

那么如何实现情景二中的应用呢?答案就是使用友元函数。

//声明
friend Rectangle operator*(int arg, const Rectangle & rect);

//定义
Rectangle operator*(int arg, const Rectangle & rect){
	Rectangle result;
	result.height = rect.height * arg;
	result.width = rect.width * arg;
	return result;
}
//注:千万不要写成Rectangle Rectangle::operator*....
//因为此处定义的友元函数并不是类的成员函数

友元函数不是类的成员函数,但是可以在访问到类的成员变量,这是其特别的地方。

典型使用示例,与cout结合,重载«

friend std::ostream & operator<<(std::ostream & out, const Rectangle & rect);

std::ostream & operator<<(std::ostream & out, const Rectangle & rect){
	out << "(" << rect.height << "," << rect.width << ")" << std::endl;
	return out;
}

Rectangle rect1(1, 1);
Rectangle rect2(2, 2);
std::cout << "rect1 is " << rect1 << "rect2 is " << rect2;

类的自动转换和强制类型转换

自动转换:

int i = 1.2;
double j = 9;

C++不会自动转换不兼容的类型,所以如果要使用自动类型转换这一特性,需要用户自己来定义。

我们以之前例子中的Rectangle类来说明如何实现不同类型间的自动类型转换

//定义构造函数,用于定义正方形,使用一个int形参。
Rectangle(int hw){
	this->height = hw;
	this->width = hw;
}

//自动转换
Rectangle rect = 1;

例子中定义的构造函数,可以将一个int型转换为Rectangle类型。由这里可以看出,只有接受一个参数的构造函数才能作为转换函数。

关键词explicit用来说明,函数只能用于显示转换。

explicit Rectangle(int hw);

//显式强制类型转换,上一个例子中的声明编译不能通过
Rectangle rect = (Rectangle) 6;

构造函数可以用于将int转换为Rectangle,但是Rectangle怎样自动转换成int呢?(其实这种转换好不现实,汗。。。这里只用于说明转换函数)

//转换函数:operator typeName();
operator int() const;
Rectangle::operator int() const {
	return this->height;
}
//请注意:转换函数没有返回类型,operator并不是返回类型;同时不能有参数

Rectangle rect = 1;
int i = rect;				//隐式转换
int i = Rectangle(rect);	//显式转换

静态成员变量

注意点:不能在类声明中初始化。

//声明中
static int number;

//初始化,在声明之外的文件中
int StringBad::number = 0;

类声明位于头文件中,程序可能在多个地方包含头文件,如果在头文件中进行初始化,就会出现多个初始化语句,从而引发错误。但是,如果静态成员是const整数类型,或者枚举型,则可以在类声明中进行初始化。(在编译时就进行了初始化替换)

特殊成员函数

构造函数和析构函数是特殊成员函数,如果声明一个类,会默认包含这两个函数。那么是否还有其他的特殊成员函数呢?

先看下面这个栗子有没有问题:

private:
	char * str;
	int len;
	static int number;

int StringBad::number = 0;
StringBad::StringBad(const char * s){
	len = std::strlen(s) + 1;
	str = new char[len];
	std::strcpy(str, s);
	number++;
	std::cout << number << " :\"" << str << "\" String object created." << std::endl;
}
StringBad::~StringBad(){
	std::cout << "\"" << str << "\" String object deleted." << std::endl;
	--number;
	std::cout << number << " objects left." << std::endl;
}

上面是一个类似String类的一个类定义,静态成员变量number用于保存对象的声明个数。现在有下面这种使用情形:

StringBad bad1("bad one");
StringBad bad2("bad two");
StringBad bad3 = bad1;

从图中我们看到bad1被删除了2次,导致number值变成了1。主要原因是这一句:StringBad bad3 = bad1;,执行过程中,赋值运算符和复制构造函数 被调用。

特殊成员函数:

  • 默认构造函数/析构函数
  • 复制构造函数
  • 赋值运算符
  • 地址运算符
  • 移动构造函数
  • 移动赋值运算符

特殊成员函数都是自动定义的。下面对这几个特殊成员函数作详细介绍,默认构造函数和析构函数就直接跳过了,这个很简单,相信大家都清楚。地址运算符就是返回调用对象的地址。

关于移动构造函数和移动赋值运算符,后面再另立文章说明。

复制构造函数

上面的那一句赋值语句等效于:

StringBad bad3 = new StringBad(bad1);

此处的复制构造函数的原型:

StringBad(const StringBad &);

当新建一个对象并将其初始化为同类现有对象时,复制构造函数会被调用。

一. 以下4种声明都会调用复制构造函数:

StringBad bad3 = StringBad(bad1);
StringBad bad3(bad1);
StringBad bad3 = bad1;
StringBad * bad3 = new StringBad(bad1);

二. 当程序生成对象副本时,编译器也会调用复制构造函数:

void callStr(StringBad bad){
	std::cout << bad <<endl;
}

callStr(bad1);

当程序调用callStr函数时,需要创建一个bad1对象的副本,这时会调用复制构造函数。正因如此,所以尽量使用引用传递,节省调用复制构造函数的时间和存储新对象的空间。

默认的复制构造函数的功能:复制每个非静态成员(浅复制),如果成员本身就是类对象,将使用这个类的复制构造函数来复制成员对象。显然,默认构造函数通常并不能满足要求。

通常,复制构造函数应该复制数据,而不仅是数据的地址;另外需要更新所有受影响的静态类成员。如下所示:

StringBad::StringBad(const StringBad & bad){
	number++;	//静态类成员
	len = bad.len;
	str = new char[len + 1];
	std::strcpy(str , bad.str);		//数据拷贝
}

赋值运算符

原型:

StringBad & StringBad::operator=(const StringBad &);

使用情景:将已有的对象赋值给另一个对象时。

StringBad bad3;
bad3 = bad1; 	//bad1已经初始化

StringBad bad3 = bad1; //调用复制构造函数

赋值运算符与复制构造函数存在的问题类似,也是使用的浅复制。

所以,通常,我们需要为赋值运算符定义以下功能:

  1. 检查自我赋值情况
  2. 释放成员指针以前指向的内存
  3. 复制数据,而不仅仅是数据的地址
  4. 返回一个指向调用对象的引用

StringBad类的赋值运算符:

StringBad & StringBad::operator=(const StringBad & bad){
	if(this == bad)		//自我赋值检查
		return *this;
	delete [] str;		//删除原str
	len = bad.len;
	str = new char[len+1];
	std::strcpy(str, bad.str);		//复制数据,不是地址
	return *this;					//返回调用对象的引用
}

参考

  1. 《C++ Primer>
  2. C++文档:http://www.cplusplus.com/doc/