IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> More Effective C++ -> 正文阅读

[C++知识库]More Effective C++

Scott Meyers大师Effective三部曲:Effective C++、More Effective C++、Effective STL。

1. 指针与引用的区别

相同点:指针和引用都是让你间接引用其它对象。

不同点:

1.在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。在C++里,引用应被初始化。 不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。

2.指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。

    std::string s1("Nancy");
	std::string s2("Clancy");
	std::string& rs = s1; // rs引用s1
	std::string* ps = &s1; // ps指向s1
	rs = s2; // rs仍旧引用s1,但是s1的值现在是"Clancy"

参考:c++ 引用和指针_baidu_16370559的博客-CSDN博客

2. 尽量使用C++风格的类型转换

这四个操作符是:static_cast、const_cast、dynamic_cast、reinterpret_cast。

参考:c++ 4种新型的类型static_cast、dynamic_cast、reinterpret_cast、const_cast转换运算符_baidu_16370559的博客-CSDN博客

3.?不要对数组使用多态?

在对数组进行传参使用多态时,程序会crash; 因为数组在移位至下一数据时,步长是形参(基类)的size,而不是指针实际指向数据类型(派生类)的size,所以会数组会移位至一个非法的地址?。

#include <iostream>
using namespace std;
 
class Base
{
public:
  virtual void test()
  {
    cout<<"Base::test()"<<endl;
  }
  int a;
};
 
class Derived: public Base
{
public:
   void test()
  {
    cout<<"Derived::test()"<<endl;
  }
  int b, c;
};
 
void testArray(Base bArray[], int n)
{
  for(int i =0; i<n; i++)
    bArray[i].test();  //i = 1时,程序crash; 编译器原先已经假设数组中元素与Base对象的大小一致,但是现在数组中每一个对象大小却与Derived一致,派生类的长度比基类要长,数组将移动到一个非法位置。
}
 
int main()
{
  Base *p = new Derived[2];  
  testArray(p, 2);    
}
 

?4.?避免无用的缺省构造函数

对于很多对象来说,不利用外部数据进行完全的初始化是不合理的.

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber) {}
	virtual ~EquipmentPiece() {}
	int a = 1;
	float b = 2.0;
};

   //避免无用的缺省构造函数
	int ID1 = 1, ID2 = 2;
	EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) }; // 正确,提供了构造函数的参数

	// 利用指针数组来代替一个对象数组
	typedef EquipmentPiece* PEP; // PEP指针指向一个EquipmentPiece对象
	PEP* bestPieces5 = new PEP[10]; // 也正确
	// 在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象
	for (int i = 0; i < 10; ++i)
		bestPieces5[i] = new EquipmentPiece(ID1);
	for (int i = 0; i < 10; ++i)
		delete bestPieces5[i];
	delete bestPieces5;

利用指针数组代替一个对象数组这种方法有两个缺点:第一你必须删除数组里每个指针所指向的对象。如果忘了,就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针.

解决办法:

	void* rawMemory = operator new[](10 * sizeof(EquipmentPiece));
	EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);

	for (int i = 0; i < 10; ++i)
		new(&bestPieces6[i]) EquipmentPiece(i);

	for (int i = 9; i >= 0; --i)
		bestPieces6[i].~EquipmentPiece(); // 如果使用普通的数组删除方法,程序的运行将是不可预测的

	operator delete[](rawMemory);

参考:c++ new操作符(new operator)、operator new、placement new 、operator new[] 及相对应的delete 操作符、operator delete_baidu_16370559的博客-CSDN博客

5.?谨慎定义类型转换函数?

有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符

隐式类型转换运算符只是一个样子奇怪的成员函数:operator关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1) // 转换int到有理数类
	{
		n = numerator;
		d = denominator;
	}

	operator double() const // 转换Rational类成double类型
	{
		return static_cast<double>(n) / d;
	}

	double asDouble() const
	{
		return static_cast<double>(n) / d;
	}

private:
	int n, d;
};

	//谨慎定义类型转换函数
	Rational r(1, 2); // r的值是1/2
	double d = 0.5 * r; // 转换r到double,然后做乘法
	fprintf(stdout, "value: %f\n", d);
	std::cout << r << std::endl; // 应该打印出"1/2",但事与愿违,是一个浮点数,而不是一个有理数,隐式类型转换的缺点

单参数构造函数是指只用一个参数即可调用的构造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值。 同时因为默认是implicit(隐式)。

explicit关键字是为了解决隐式类型转换而特别引入的这个特性。如果构造函数用explicit声明,编译器会拒绝为了隐式类型转换而调用构造函数。

template<class T>
class Array {
public:
	Array(int lowBound, int highBound) {}
	explicit Array(int size) {}
public:
	T& operator[](int index) { return data[index]; }



private:
	T* data;
};
bool operator== (const Array<int>& lhs, const Array<int>& rhs)
{
	return false;
}
	Array<int> a(10);
	Array<int> b(10);
	for (int i = 0; i < 10; ++i) {
		//if (a == b[i]) {} // 如果构造函数Array(int size)没有explicit关键字,编译器将能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数,加上explicit关键字则可避免隐式转换

		if (a == Array<int>(b[i])) {} // 正确,显示从int到Array<int>转换(但是代码的逻辑不合理)
		if (a == static_cast<Array<int>>(b[i])) {} // 同样正确,同样不合理
		if (a == (Array<int>)b[i]) {} // C风格的转换也正确,但是逻辑依旧不合理
	}

6. 自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别?

C++规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0作为int参数的值给该函数。

前缀形式有时叫做”增加然后取回”, 返回的是引用,效率高

后缀形式叫做”取回然后增加”。返回的是新的值,值和原来的一样。效率低。


class UPInt { // unlimited precision int
public:
	// 注意:前缀与后缀形式返回值类型是不同的,前缀形式返回一个引用,后缀形式返回一个const类型
	UPInt& operator++() // ++前缀
	{
		//*this += 1; // 增加
		i += 1;
		return *this; // 取回值
	}
 
	const UPInt operator++(int) // ++后缀
	{
		// 注意:建立了一个显示的临时对象,这个临时对象必须被构造并在最后被析构,前缀没有这样的临时对象
		UPInt oldValue = *this; // 取回值
		// 后缀应该根据它们的前缀形式来实现
		++(*this); // 增加
		return oldValue; // 返回被取回的值
	}
 
	UPInt& operator--() // --前缀
	{
		i -= 1;
		return *this;
	}
 
	const UPInt operator--(int) // --后缀
	{
		UPInt oldValue = *this;
		--(*this);
		return oldValue;
	}
};

7.?不要重载”&&”, “||”,或”,”

与C一样,C++使用布尔表达式短路求值法(short-circuit evaluation)。这表示一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。

你不能重载下面的操作符:
.? .*? ?::? ? ?:
new? delete? sizeof? typeid
static_cast dynamic_cast const_cast reinterpret_cast
你能重载:
operator new? ? ?operator delete
operator new[]? ? operator delete[]
+? -? *? /? %? ^? &? | ~
!? =? <? >? +=? -=? *=? /=? %=
^=? &=? |=? <<? >>? >>=? <<=? ==? !=
<=? >=? &&? ||? + + --? ,? ->*? ->
()? []?

8.?理解各种不同含义的new和delete

new操作符(new operator)和new操作(operator new)的区别:

new操作符就像sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。你所能改变的是如何为对象分配内存。new操作符调用一个函数来完成必须的内存分配,你能够重写或重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字是operator new。

函数operator new通常声明:返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。参数size_t确定分配多少内存。你能增加额外的参数重载函数operator new,但是第一个参数类型必须是size_t。就像malloc一样,operator new的职责只是分配内存。它对构造函数一无所知。把operator new返回的未经处理的指针传递给一个对象是new操作符的工作。

placement new:特殊的operator new,接受的参数除了size_t外还有其它。

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber) {}
	virtual ~EquipmentPiece() {}
	int a = 1;
	float b = 2.0;
};
	void* rawMemorysingle = operator new(sizeof(EquipmentPiece));
	EquipmentPiece* bestPiecesrawMemorysingle = static_cast<EquipmentPiece*>(rawMemorysingle);
	new(bestPiecesrawMemorysingle) EquipmentPiece(1);
	bestPiecesrawMemorysingle->~EquipmentPiece();
	operator delete(rawMemorysingle);


	//避免无用的缺省构造函数
	int ID1 = 1, ID2 = 2;
	EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) }; // 正确,提供了构造函数的参数

	// 利用指针数组来代替一个对象数组
	typedef EquipmentPiece* PEP; // PEP指针指向一个EquipmentPiece对象
	PEP* bestPieces5 = new PEP[10]; // 也正确
	// 在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象
	for (int i = 0; i < 10; ++i)
		bestPieces5[i] = new EquipmentPiece(ID1);
	for (int i = 0; i < 10; ++i)
		delete bestPieces5[i];
	delete bestPieces5;

	void* rawMemory = operator new[](10 * sizeof(EquipmentPiece));
	EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);

	for (int i = 0; i < 10; ++i)
		new(&bestPieces6[i]) EquipmentPiece(i);

	for (int i = 9; i >= 0; --i)
		bestPieces6[i].~EquipmentPiece(); // 如果使用普通的数组删除方法,程序的运行将是不可预测的

	operator delete[](rawMemory);


参考:c++ new操作符(new operator)、operator new、placement new 、operator new[] 及相对应的delete 操作符、operator delete_baidu_16370559的博客-CSDN博客

9. 使用析构函数防止资源泄漏

用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。

资源应该被封装在一个对象里,遵循这个规则,你通常就能够避免在存在异常环境里发生资源泄漏,通过智能指针的方式。

C++确保删除空指针是安全的,所以析构函数在删除指针前不需要检测这些指针是否指向了某些对象。

延伸

1.智能指针,参考c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)_baidu_16370559的博客-CSDN博客

2.异常:c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客?

?10. 在构造函数中防止资源泄漏

C++仅仅能删除被完全构造的对象(fully constructed objects),只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。C++拒绝为没有完成构造操作的对象调用析构函数。

具体表现为:在执行构造函数函数体是抛出异常,该类的成员变量已经被完全构造,可以自动删除掉。而本身没有完全构造,其本身的析构函数不会被调用。

如果使用new 的方式创建对象即A *pa = new A();

如果A的构造函数有异常,A没有被完全构造,new 操作失败,返回的指针pa为空,后面使用delete pa;也不会调用 A的析构函数。

在构造函数中可以使用try catch throw捕获所有的异常。更好的解决方法是通过智能指针的方式。

参考:C++构造函数初始化列表与赋值_baidu_16370559的博客-CSDN博客

11.?禁止异常信息(exceptions)传递到析构函数外

禁止异常传递到析构函数外有两个原因:第一能够在异常传递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。

解决办法使用 try catch 函数

延伸:

1.C++中处理异常的过程是这样的:在执行程序发生异常,可以不在本函数中处理,而是抛出一个错误信息,把它传递给上一级的函数来解决,上一级解决不了,再传给其上一级,由其上一级处理。如此逐级上传,直到最高一级还无法处理的话,运行系统会自动调用系统函数terminate.

关于try catch throw 知识参考:

c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客

2.这个和effective c++ 条款8?别让异常逃离析构函数 一样的参考effective c++ 学习_baidu_16370559的博客-CSDN博客

12.?理解”抛出一个异常”与”传递一个参数”或”调用一个虚函数”间的差异

你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

C++规范要求被作为异常抛出的对象必须被复制。即使被抛出的对象不会被释放,也会进行拷贝操作。抛出异常运行速度比参数传递要慢。

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。

catch子句中进行异常匹配时可以进行两种类型转换:第一种是派生类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常。这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void*指针的catch子句能捕获任何类型的指针类型异常。

catch子句匹配顺序总是取决于它们在程序中出现的顺序。异常传递到 catch 子句中有三种方式:通过指针(by pointer),通过传值(by value)或通过引用(by reference),其中引用最好

参考见:c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客

13. 通过引用(reference)捕获异常

异常传递到 catch 子句中有三种方式:通过指针(by pointer),通过传值(by value)或通过引用(by reference),其中引用最好.

1..通过指针的方式,对静态异常变量catch中不需要delete,但堆上异常变量需要delete,因此较复杂。而且通过指针捕获异常也不符合C++语言本身的规范。四个标准的异常----bad_alloc(当operator new不能分配足够的内存时被抛出);bad_cast(当dynamic_cast针对一个引用(reference)操作失败时被抛出);bad_typeid(当dynamic_cast对空指针进行操作时被抛出);bad_exception(用于unexpected异常)----都不是指向对象的指针,所以你必须通过值或引用来捕获它们。

2.通过传值时,需要进行拷贝两次(离开作用域一次,catch接收一次),而且它会产生 slicing problem(切割问题),即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。这样的sliced对象实际上是一个基类对象:它们没有派生类的数据成员,而且当本准备调用它们的虚拟函数时,系统解析后调用的却是基类对象的函数。

3.异常变量复制一次,避免了上述所有问题

参考:c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客

14. 审慎使用异常规格(exception specifications)

如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数std::unexpected将被自动地调用(This function is automatically called when a function throws an exception that is not listed in its dynamic-exception-specifier.)。std::unexpected缺省的行为是调用函数std::terminate,而std::terminate缺省的行为是调用函数abort。应避免调用std::unexpected。

避免在带有类型参数的模板内使用异常规格。

C++允许你用其它不同的异常类型替换std::unexpected异常,通过std::set_unexpected

15.?了解异常处理的系统开销

采用不支持异常的方法编译的程序一般比支持异常的程序运行速度更快所占空间也更小。

为了减少开销,你应该避免使用无用的try块。如果使用try块,代码的尺寸将增加并且运行速度也会减慢。

16. 牢记80-20准则(80-20 rule)

80-20准则说的是大约20%的代码使用了80%的程序资源;大约20%的代码耗用了大约80%的运行时间;大约20%的代码使用了80%的内存;大约20%的代码执行80%的磁盘访问;80%的维护投入于大约20%的代码上。基本的观点:软件整体的性能取决于代码组成中的一小部分。
?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-10 22:13:22  更:2022-03-10 22:15:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:50:05-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码