C++基础 Effective C++第二章(构造/析构/赋值运算)

      条款05:了解C++默默编写并调用哪些函数

      C++不允许改变引用的指向,如果类里面有一个引用成员,或内含const成员,当在进行类对象赋值的时候,编译器会拒绝。如果某个基类将拷贝构造函数声明为private,编译器将拒绝为其derived class生成一个拷贝操作符。

      条款6:若不想使用编译器自动生成的函数,就该明确拒绝

      如果要阻止一个类进行拷贝和赋值,一种方法,自己定义他们并设为private, 这可以阻止编译器生成公共的这些函数,防止在类外进行拷贝或赋值。但是成员函数和友元函数还可以调用,所以可以使用以下方法:

     1 将其声明为private,但是不进行定义,这样成员函数与友元函数内进行拷贝或赋值,链接器会发生错误,这将编译器错误移到链接器,在C++ iostream程序库中为了组织copying行为,使用的就是这个方法,将拷贝构造函数,赋值函数声明为私有,并且只声明,不进行定义。如果进行了这 些操作,则链接器发生错误返回。

    2 如果将链接期的错误移至编译期,则可以构造一个专门为了阻止copying而设计的基类,且基类的析够函数不一定是virtual,不包含任何数据。然后使该类继承它,不一定要共有继承,可以私有继承。如下是两种方式:

   

//构造一个不能进行拷贝和赋值的类
//#include <iostream>
//using namespace std;

class HomeForSale
{
public:
	HomeForSale() {}
private:
	//以下两个函数只声明不定义,编译通过,如果不进行拷贝和赋值,
	//则一起正常,否则链接器错误
    HomeForSale(const HomeForSale&);
	HomeForSale& operator=(const HomeForSale&);
};

//定义一个不能拷贝的类,允许继承对象构造和析构,不允许拷贝和赋值
class Uncopyable
{
protected:
	Uncopyable() {}
	~Uncopyable() {}
private:
	Uncopyable(const Uncopyable&);
	Uncopyable& operator=(const Uncopyable&);
};

//继承类继承它,则编译器会拒绝为其生成拷贝构造函数与赋值函数
class HomeForSale1 : private Uncopyable
{
public:
    HomeForSale1() {}
};


int main()
{
	HomeForSale1 h;
	return 0;
}

        条款07:为多态基类声明virtual析够函数

        C++标准库的容器:vector, list, set, map都不含有任何虚函数,所以不应该继承任何一个容器,否则会有内存泄漏。当一个类的目前不是是作为基类使用,或不是为了具备多态,就不应该将析构函数声明为虚的。

         如果一个类的目的是用作基类,并且含有虚函数,应该为其定义虚析够函数,这样当一个基类指针通过new指向继承对象时,当删除这个基类时,会调用继承类的 析构函数,防止内存泄漏。也可以将其声明为虚基类,析构函数声明为pure virtual析构函数,但是为了编译器正常调用,需要在类外部提供一份纯虚析够函数的定义。

        条款08:别让异常逃离析够函数

       析够函数绝对不要吐出异常。如果一个被析够函数调用的函数可能抛出异常,析够函数应该能捕捉异常,然后吞下它们(不传播)或结束程序。如以下,

//不要让异常逃离析构函数

class DBConnection
{
public:
	static DBConnection create();
	void close();
};

//定义一个类管理DBConnection对象,如果close()抛出异常,应该捕捉它,
//不应该让它逃离,捕捉异常后,记录调用失败,然后 1 程序结束  2 吞下异常,
class DBConn
{
public:
	~DBConn()
	{
		try
		{
		   db.close();
		}
		catch(...) //捕获所有异常
		{
			//记录异常
			std::abort(); //可以调用该函数直接结束程序,或不结束。
		}

	}
private:
	DBConnection db;
};

//一个较好的是重新设置DBConn接口,提供一个接口使客户可以有机会处理出现的问题
class DBConn
{
public:
	void close()
	{
		db.close();
		closed = true;
	}

	~DBConn()
	{
		if (!closed)
		{
			try
			{
				db.close();
			}
			catch(...)
			{
				//纪录调用失败
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};
       上述的重写设计DBconn的方法,是可以给客户一个机会自己处理可能出现的问题,所以需要提供一个close接口,使客户显示调用来关闭数据库,但如果 他们没有调用close()而依赖析构函数的调用,则如果错误发生,则又成为析构函数处理这个异常了。一个原则就是如果某个操作可能在失败的时候抛出异 常,而又存在某种需要处理该异常,这个异常必须来自析构函数以外的某个函数。

        如果客户需要对某个操作函数运行期间抛出的异常做出反应,class应该提供一个普通函数(而非在析构函数中)执行该操作。

       条款09:绝不要在构造和析够函数过程中调用virtual函数

       如以下继承体系:   

//如有以下继承体系,希望每创建一个交易对象,都会有一笔日志记录
class Transaction
{
public:
	Transaction() 
	{
		logTransaction();
	};

	virtual void logTransaction() const = 0;
};

//继承类,需要实现logTransaction()
class BuyTransaction : public Transaction
{
public:
	virtual void logTransaction() const;
};

//继承类,需要实现logTransaction()
class SellTransaction : public Transaction
{
public:
	virtual void logTransaction() const;
};
       原意是希望借助构造函数在每次构造一个继承类对象时,就进行一笔日志记录,这样的行为就像我正在构造继承类对象的基类部分,自己还没有构造完成,然后又下 降到继承类去调用继承类的一个函数,而现在还没有继承类,所以不可能调用继承类的,对象只是基类对象,所以只会调用基类的log处理,并不能实现所期盼的 行为。

       base class构造期间virtual函数绝不会下降到derived class。所以对象的行为就像隶属基类一样。即在基类构造期间,virtual函数不是虚函数。在继承类对象的基类对象构造期间,对象的类型是基类,而 不是继承类,如果这时调用virtual或使用运行其类型信息(dynamic_cast和typeid),也会把对象视为基类类型。析构函数也如此,一 旦继承类的析构函数开始执行,继承类成员变量便呈现未定义值,进入基类析够函数后对象就成为一个base class对象,虚函数与dynamic_casts也只能将该对象视为基类对象。

       如果要确保每次一有Transaction继承体系上对象被创建,就会有适当的logTraction被调用的方法,实现上面的需求,一种方法是可以在基类内将logTransaction更改为非虚,然后要求继承类构造函数传递必要的信息给基类构造函数:

      

//解决方案:logTransaction改为非虚,然后继承类构造函数传递必要的信息给基类构造函数
class Transaction
{
public:
	explicit Transaction(const std::string& logInfo)
	{
		logTransaction(logInfo);//将继承类信息传递过来
	}

	void logTransaction(const std::string& logInfo) const; //非虚
};

class BuyTransaction : public Transaction
{
public:
	BuyTransaction(parameters) 
		: Transaction(createLogString(parameters))
	{}

private:
	//静态的放置“初期未成熟的buytransaction对象内尚未初始化的成员变量”
	static std::string createLogString(parameters); 
};

       上面的方法就是,如果无法使用虚函数从基类向下调用,则可以藉由“令derived classes将必要的构造函数信息向上传递止base class构造函数”来加以弥补。

       请记住:在构造和析构期间不要调用virtual函数,因为这类调用不会下降至继承类(比起当前执行构造函数和析构函数那层)

      条款10:令operator=返回一个reference to *this

     为了实现连锁赋值,(x=y=z=15),赋值操作符必须返回一个reference指向操作符的左侧实参(为啥呀。。)。

     这是一个协议,也适用于赋值相关运算(+=等), 并不一定到遵守,但是如果不这么写也可以通过编译,。但标准库类型string, vector,complex,tr1::shared_ptr等共同遵守。

  

        条款11:令operator=中处理自我赋值

        处理自我赋值有3中解决方案:1 证同测试,2 合理安排语句顺序保证代码的异常安全  3 copy and swap, 如以下例子

//在operator=中处理自我赋值
class Bitmap {};

//保存一个指针指向一块动态分配的bitmap
class Widget
{
public:
	//问题的发生:
    Widget& operator=(const Widget& rhs)
	{
		delete pb;
		//如果rhs与this指向同一块内存则错误
		pb = new Bitmap(*rhs.pb);
		return *this;
	}

	//解决方法1:证同测试,
	Widget& operator= (const Widget& rhs)
	{
		if (this == &rhs)
			return *this;
		delete pb;
		//这里可能出现异常问题,如果new发生异常(不论内存不足还是copy构造函数异常)
		//widget会持有一个指针指向一块被删除的bitmap.
		pb = new Bitmap(*rhs.pb);
		return *this;
	}

	//解决方法2:精心安排语句顺序来保证“异常安全”,防止解法1的问题
	Widget& operator=(const Widget& rhs)
	{
		//记住原先指针,再构造一个副本,然后再删除,即删除在构造之后
		Bitmap* porg = pb;
		pb = new Bitmap(*rhs.pb);
		delete porg;
		return *this;
	}

	
	//解法3:2的一个替代方案,即copy and swap技术
	void swap(Widget& rhs)
	{
		//交换数据
	}

	Widget& operator=(const Widget& rhs)
	{
		Widget temp(rhs);
		swap(temp);   //交换*this与temp的数据
		return *this;
	}

	//3的另一个变型解法,依赖以下事实(1) 某类的拷贝赋值操作可能被声明为"by value"方式接受实参
	//(2)以by value方式传递东西会造成另一份副本
	Widget* operator=(Widget rhs)  //这里利用by value构造一个副本
	{
		swap(rhs);   //这里是将*this与副本数据互换,
		return *this;
	}
private:
	Bitmap* pb;  //指向一个从堆上分配的对象
};


      在最后一个变型解法中实际上是将在函数内调用赋值构造函数的时机转移到了参数构造阶段来构造一个副本,利用编译器的特性,会比较高校,但是作者认为这个做法牺牲了清晰性。我也觉得2,3解法就够好了,3的变型可有可无。

       条款12:复制对象时勿忘其每一个成分

       当自己编写一个拷贝函数,要确保1 复制所有的local成员变量,2 调用所有base calsses内的适当的copy函数。

       不要在赋值函数里调用拷贝构造函数,因为拷贝构造函数的作用是构造对象,所以在赋值函数中调用赋值构造函数相当于重新构造一个已经存在的对象,这是荒谬的。

       不要在拷贝构造函数里调用赋值函数,同样因为拷贝函数用来构造对象,而赋值函数作用于已初始化的对象身上,而在拷贝构造函数时对象尚未构造完成,所以就像在一个尚未初始化的对象身上做”只对已初始化对象才有意义“的事一样。

       如果拷贝构造函数与拷贝赋值函数有相似的代码,可以建立一个新的成员函数,通常是private且命名为init,然后令二者调用该函数,这就可以消除重复的代码了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 本章主要针对初学者,简要介绍了c/c++的基本语法和常见的编译错误。首先,介绍了程序的基本结构和常用的数据类型。c语言的基本语句包括赋值语句、条件语句和循环语句等,这些语句都可以组成程序的逻辑结构。接下来,介绍了函数的定义和调用,以及参数传递的方式。对于c++,还介绍了一些面向对象的概念,例如类、对象、成员函数等。同时,也提到了头文件和命名空间的使用方法。 在编程过程中,常常会出现各种编译错误,例如语法错误、类型不匹配、语义错误等,需要学会如何查看和解决这些错误。此外,还介绍了调试工具和各种常用的运算符和表达式,这些都是初学者必须掌握的基础知识。 总的来说,本章是关于c/c++快速入门的一篇简介性文章。虽然只是涉及到了基础的语法和知识点,但对于初学者而言是一个很好的起点。在学习过程中需要不断实践,积累经验,并不断深入了解更高级的编程技术和工具。 ### 回答2: C/C++是一种广泛使用的编程语言,它具有高效、灵活、可移植等特点,在各个领域得到广泛应用。C语言是C++语言的基础,在学习C++之前,需要先掌握C语言的基础知识。 本文介绍的是C/C++教程中的第二章——快速入门C/C++。在这一章节中,我们将介绍C语言的基本语法、变量、运算符和流程控制语句等基础知识。以下是C语言的一些基本知识点。 C语言的基本语法: C语言程序由多个函数组成,其中一个函数必须命名为main(),程序从该函数开始执行。C程序中的语句以分号结束,注释使用“//”表示单行注释,“/* */”表示多行注释。 变量和数据类型: C语言中变量的定义格式为“数据类型 变量名”;数据类型包括基本类型和用户自定义类型。C语言中的基本类型有int类型、char类型、float类型和double类型等。其中,int类型表示整型,char类型表示字符型,float类型和double类型表示浮点型。 运算符: C语言中的运算符包括算术运算符、关系运算符、逻辑运算符等。例如,“+”表示加法运算符,“>=”表示大于等于运算符,“&&”表示逻辑与运算符。 流程控制语句: C语言中的流程控制语句包括if语句、switch语句、while语句、do-while语句和for语句等。这些语句可以根据条件执行相应的语句块。 总之,本章节的快速入门C/C++,具有基本语法、变量、运算符和流程控制语句等基础知识。初学者可以通过这些基础知识,轻松入门C/C++,为后续学习打下基础。同时,要注意编写代码的规范和逻辑性,才能更好的理解和使用C/C++语言。 ### 回答3: C语言是一门广泛使用的编程语言,具有高效、灵活、稳定等特点,被广泛应用于嵌入式系统、操作系统、驱动程序、多媒体应用等领域。学习C语言是程序员的必备技能之一。 第二章的快速入门C/C++教程,主要介绍了C/C++语言的基础知识,重点是程序的结构和输入输出。其中程序结构包括函数、语句、变量、表达式等,而输入输出则包括scanf、printf、getchar和putchar等函数。 # 程序结构 程序结构是指程序的基本构成单元,包括函数、语句、变量、表达式等。C语言中的程序结构主要包含以下几个方面。 ## 函数 C语言中,函数是程序的基本组成单元。一个C程序可以由一个或多个函数组成,每个函数可以完成一个任务。函数的格式如下: ```c 返回类型 函数名(参数1, 参数2, ...){ // 函数体 return 返回值; } ``` 其中,返回类型指函数执行后的返回值类型;函数名是由程序员定义的,用于调用函数时识别函数;参数列表是函数的输入参数,可以有多个参数,每个参数由类型和变量名组成;函数体是函数要执行的代码块;return语句可以返回函数的执行结果。 ## 语句 语句是完成特定功能的一组指令。C语言中的语句包括赋值语句、条件语句、循环语句等。C语言中通常使用花括号来表示语句块。例如,下面是一个if语句的例子。 ```c if(条件){ // if语句块 }else{ // else语句块 } ``` 如果条件为真,则执行if语句块中的代码;否则执行else语句块中的代码。 ## 变量 变量是用于存储数据的一种容器。在C语言中,一个变量包括变量名、类型和值。变量名由程序员定义,用于识别变量;类型指变量的数据类型,如整型、字符型、实型等;值是存储在变量中的数据。变量的定义格式如下。 ```c 数据类型 变量名 = 值; ``` 例如,下面是一个整型变量的定义。 ```c int num = 10; ``` ## 表达式 表达式是由变量、运算符和常量组成的一个具有返回值的语句。C语言中的运算符分为算术运算符、关系运算符、逻辑运算符等,例如加号、减号、乘号、除号等。下面是一个简单的表达式。 ```c a = 5 + 6 * 3 / 2 - 1; ``` 这个表达式将计算5加6乘3除以2减1的值,并将结果赋给a变量。 # 输入输出 输入输出是程序中常重要的部分,可以让程序与用户进行交互。C语言中有多种输入输出函数,其中一些最常用的是scanf、printf、getchar和putchar函数。 ## scanf函数 scanf函数用于从标准输入读取格式化数据,并将读取的数据存储到变量中。它的格式如下。 ```c scanf("格式控制字符串", 变量列表); ``` 其中,格式控制字符串指示scanf函数需要读取的数据类型和格式,变量列表指向要读取的变量。下面是一个scanf函数的例子。 ```c int num; scanf("%d", &num); ``` 这个代码段将从标准输入读取一个整数,并将其存储到num变量中。 ## printf函数 printf函数用于将格式化数据输出到标准输出。它的格式如下。 ```c printf("格式控制字符串", 参数列表); ``` 其中,格式控制字符串指示printf函数需要输出的数据类型和格式,参数列表包含要输出的变量和常量。下面是一个printf函数的例子。 ```c int num = 5; printf("num的值是%d\n", num); ``` 这个代码段将输出“num的值是5”。 ## getchar和putchar函数 getchar函数用于从标准输入读取一个字符,putchar函数用于将一个字符输出到标准输出。它们的用法常简单,例如下面的代码将读取一个字符并将其转换成大写字母后输出。 ```c char c = getchar(); putchar(toupper(c)); ``` 以上就是第二章中的快速入门C/C++教程的主要内容,包括程序结构和输入输出方面的基础知识。熟练掌握这些内容,对于学习C语言来说是常重要的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值