【Effective C++】【Accustoming yourself to C++】

文章目录

  • term1:View C++as a federation of languages
    • (1)C
    • (2)Object-Oriented C++
    • (3)Template C++
    • (4)STL
  • term2:Prefer const,enums,inlines to #defines
    • (1)const:常量替换
    • (2)enums :枚举量替换
    • (3)inlines : 内置函数替换
  • term3:Use const whenever possible
    • (1)const 修饰指针,迭代器,函数返回值。
    • (2)const修饰成员函数
  • term4:Make sure that objects are initialized before they're used
    • (1)内置数据类型
    • (2)自定义数据类型
    • (3)使用local_static对象替换none_local_static对象
  • 4、总结
  • 5、参考

term1:View C++as a federation of languages

将C++看成一个语言联邦,C++有一些强烈的特性,为了方便理解,作者将其总结为次语言

(1)C

C++是以C为基础的,区块,语句,预处理器,内置数据类型,指针等诸多概念都是来自C。C++可以用来处理大型的复杂项目;而当前的C语言,主要用来编写51,32的MCU开发,底层驱动。

(2)Object-Oriented C++

class,封装,继承,多态,虚函数(动态绑定等等),这一部分是面向对象的设计在C++上最直接的体现。

(3)Template C++

涉及泛型编程,内置数种可供套用的函数或者类。良好的编程守则“惟Templete适用”。

(4)STL

STL是个模板库,主要涉及容器,算法和迭代器
在不同情况下使用适合的部分,可以使 C++ 实现高效编程。

term2:Prefer const,enums,inlines to #defines

这个条款的本质:在编程过程中多用编译器少用预处理器。因为#define是宏定义,他不被视作语言的一部分。而const,enums,inlines对应着3种不同的情况。

(1)const:常量替换

举个栗子:

#define PI 3.14159

这段代码在程序预处理过程中就执行了,这个记号名称PI,可能没有进入记号表,也从未被编译器看见。这样的话有一个不好的后果和一个潜在的风险。不好的后果就是如果程序编译报错,因为记号表没有收录这个记号名称,他会抱3.14159出错,会给后期的排查带来很大的困难;潜在的风险就是,他会直接替换,而不会对类型进行检查。
针对记号名称没有出现在记号表有一个解决办法:

//以一个常量替换上述的宏
const double PI = 3.14159;

除了以上的常量替换,有2种情况相对来说特殊一些:
(1)定义常量指针
简而言之,有必要将指针申明为const;
(2)class专属常量
class相较于struct更加强调作用域,#defines不能满足这个要求,而且不能提供任何封装性,所以对于有些专属常量,谨慎使用#defines。

class GamePlayer{
private:
		static const int NumTurns = 5; //声明式
		int scores[NumTurns];
		//to do sth;
		//高级编译器支持
}

如果程序中需要对class内的专属常量进行取地址的操作,需要另外提供定义式

const int GamePlayer::NumTurns; //定义式

旧式的编译器不支持在变量声明时获得初始值,所以按照一贯的习惯(变量在头文件声明,在实现文件中定义)。

class CostEstimate {
	static const double PI;			//常量声明
	//to do sth;					//位于头文件

};
const double
	CostEstimate::PI = 3.14159;      //常量定义
									 //位于实现文件内

但是如果遇到上面的例子,你要声明一个数组,编译器坚持要知道数组的大小。这时候就可以使用“the enum hack"补偿做法。

(2)enums :枚举量替换

一个属于枚举类型的数值,可以权充ints使用。

class GamePlayer{
private:
	enum{NumTurns = 5};
	int scores[NumTurns];
	...
}

(3)inlines : 内置函数替换

另一种情况就是实现宏,使用宏的时候,尤其要注意,表达式外要加上足够多的括号,不然,会有一些意想不到的错误。
举个栗子:

//a,b中的较大值给到f
#define CALL_WITH_MAX(a,b) f((a) >(b) ?(a) :(b))

使用这个宏定义
int a = 5,b = 0;
CALL_WITH_MAX(++a,b);   	//a累加2次
CALL_WITH_MAX(++a,b+10);	//a累加1次

++的次数取决与和谁比较,这样的宏定义,就有很多隐藏的风险。因为宏不会像函数调用那样产生额外的开销,但与此同时会有不可预料的行为以及类型不安全的情况发生。
C++给出的解决方法是:templete inline函数

templete<typename T>
inline void callwithMax(const T&a,const T&b){
	f(a > b ? a: b);
}

但是#define在现阶段是无法被代替的,要充分了解他,并知悉他潜在的风险。

term3:Use const whenever possible

const允许你定义一个语义约束,指定一个"不该被改动的”对象。

(1)const 修饰指针,迭代器,函数返回值。

const修饰指针

char greeting[] = "Hello";
char* p = greeting;
const char* p = greeting; 		  //指向字符常量的指针,p指向的字符不能修改
char* const p = greeting; 		  //指向字符的常量指针,p本身的值不能够被修改
const char* const p = greeting;   //指向字符常量的常量指针,p指向的字符,p本身都不能被修改

为了方便理解:
在第一个声明中,p 是一个指针,它指向一个 const char。
在第二个声明中,p 是一个 const 指针,它指向一个 char。
在第三个声明中,p 是一个const指针,它指向一个const char。
const修饰迭代器
举个栗子:

std::vector<int> vec //整数类型的std::vector
...
const std::vector<int>::iterator iter = vec.begin();
//定义一个常量迭代器,相当于T* const
//迭代器不得指向不同的东西,但他所指向的东西的值是可以改动的;
*iter = 10; //正确
++iter;		//错误
std::vector<int>::const_iterator cIter = vec.begin();
//声明一个常量迭代器,相当于const T*
//迭代器不能修改容器中的元素,但是可以修改迭代器的指向;
*citer = 10;	//错误
++citer;		//正确

const修饰函数返回值

const int max(int a,int b){
	return (a > b ? a:b) ;
}
int c = 7;
max(a,b) = c;
//将一个常量的值,赋值给一个函数调用的返回值是没有意义的;
//const关键字在这里确保了函数不会进行任何可能影响程序状态
//的修改,并且使得函数的输出具有确定性。

(2)const修饰成员函数

首先要明确const在成员函数中的作用:
逻辑常量性(Logical constness):
作用: const 关键字表示该成员函数在逻辑上不会修改对象的状态。这是一种承诺,告诉编译器和其他程序员,调用这个成员函数不会改变对象的内部数据成员。
实现: 在 const 成员函数中,你不能修改非 mutable 数据成员,也不能调用非 const 成员函数。这样确保了对象在逻辑上的不变性。
允许 const 对象调用:
作用: 将 const 关键字应用于成员函数使得可以在常量对象上调用这个函数。如果一个对象被声明为 const,那么只能调用它的 const 成员函数,而不能调用普通的非 const 成员函数。
实现: 在 const 成员函数中,编译器会将 this 指针视为指向常量对象的指针,这意味着在这个函数中不能修改对象的非 mutable 成员。
我举个栗子:

class TextBlock{
public:
...
const char& operator[](std::size_t position)const
{return text[position];}
char& operator[](std::size_t position)
{return text[position];}
private:
std::string text;
};

TextBlock tb("hello");
std::cout << tb[0]; //调用non-const TextBlock::operator[]
TextBlock ctb("world");
std::cout << ctb[0]; //调用const TextBlock::operator[]

在介绍之前先引入两个概念:Bitwise Constness和Logical Constness
Bitwise Constness:
定义: “Bitwise constness” 关注的是对象在二进制表示层面是否保持不变。在这个概念中,如果对象被声明为 const,那么其底层二进制表示不应该发生变化。
实现: 通常通过将成员函数声明为 const 来实现 “bitwise constness”,以确保在 const 对象上调用这些函数时,对象的二进制表示不会发生变化。这样的函数不应修改任何非 mutable 的成员。
Logical Constness:
定义: “Logical constness” 关注的是对象的逻辑状态是否保持不变。在这个概念中,即使对象的物理状态(内部数据成员的值)发生了改变,它在外部的视角下仍然是常量。
实现: 通常通过将成员函数声明为 const,并在这些函数中不修改对象的逻辑状态。这允许在 const 对象上调用这些函数,保持对象在外部视角下的常量性。
区别:
“Bitwise constness” 关注对象底层二进制表示的变化,主要通过成员函数的 const 修饰符来实现,限制对数据成员的修改。
“Logical constness” 关注对象的逻辑状态的变化,同样通过成员函数的 const 修饰符来实现,限制对逻辑状态的修改。
在很多情况下,这两个概念是相关的,因为 “logical constness” 的实现通常涉及不修改对象的物理状态,从而保持 “bitwise constness”。然而,它们的重点略有不同,前者侧重于二进制表示,而后者更关注对象在逻辑上的不变性。

来举个栗子:

class CTextBlock{
public:
	...
	std::size_t length() const;
private:
	char* pText;
	std::size_t textLength;
	bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
	if(!lengthIsValid){
		textLength = std::strlen(pText);
		lengthIsValid = true;			//error!!!
		}
		return textLength;
}
//在const成员函数内不能赋值给textLength和lengthIsValid

用mutable释放掉non-static成员变量的bitwise constness的约束:

class CTextBlock{
public:
	...
	std::size_t length() const;
private:
	char* pText;
	mutable std::size_t textLength;
	mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
	if(!lengthIsValid){
		textLength = std::strlen(pText);
		lengthIsValid = true;			//这样就是没问题的
		}
		return textLength;
}

const成员函数不能改变(除static)成员变量的值,同理const对象不可以调用non-const函数。但是若成员变量是一个指针,仅仅改变指针指向的值却不改变指针地址,则不算事const函数,但能通过bitwise测试。
解决方法:使用mutable可以消除non-static成员变量的bitwise constness约束。
举一个其他栗子:

#include 
using namespace std;
class MyClass{
public:
	//带有mutable 关键字的非静态成员变量
	mutable int mutableData;
	//构造初始化函数 mutableData
	Myclass(int data):mutableData(data){}
	//const成员函数,逻辑上保持不变性,但允许修改mutableData
	int getData() const{
		//修改mutableData,不违反const成员函数的规定
		mutableData++;
		return mutableData
	}
}
int main(){
	Myclass obj(42);
	cout << "Initial data: " <<obj.getData()<<endl;
	const Myclass constobj(100);
	cout << "Const object data: "<<constObj.getData()<<endl;
	return 0;
}

这个函数运行后的输出结果是:

Initial data: 43
Const object data: 101

在const 和non-const成员函数中避免重复
但是很遗憾mutable不能解决所有问题,假设你要做若干操作,同时放进const和non-const的operator[]中,就会发生代码重复的问题。
举个栗子:

class TextBlock{
public:
		...
		const char& operator[](std::size_t position) const
		{
		...		//边界检验
		...		//日志记数据访问
		...		//检验数据完整性
		return text[position];
		}
		char& operator[](std::size_t position)
		{
		...		//边界检验
		...		//日志记数据访问
		...		//检验数据完整性
		return text[position];
		}
private:
		std::string text;
};

这段代码的问题很多,代码重复伴随的维护,代码膨胀等问题。最好的方法是只实现一次,但是调用的时候灵活一些。从内容看两个operator[]做的工作一样,但是留下哪个呢,留下non-const的那个,因为不论谁调用non-const operator[],都一定要首先有个non-const对象。所以利用non-const operator[]调用 const是一个避免代码重复的安全做法。
举个栗子:

class TextBlock{
public:
	...
	const char& operator[](std::size_t position) const //保持不变
	{
		...		//边界检验
		...		//日志记数据访问
		...		//检验数据完整性
		return text[position];
	}
	char& operator[](std::size_t position)
	{
		return
			const_cast<char&> //移除第一次转型添加的const
				(static_cast<const TextBlock&>(*this)[position]);
				//将TextBlock类型转化cosnt TextBlock
				//使得能够调用const operator[]
	}
	...
}

这份代码有2个转型操作,第一次用来为*this 添加const,这就告诉operator[]调用const的版本,第二次从const operator[]的返回值中移除const。

term4:Make sure that objects are initialized before they’re used

为什么对象使用之前要进行初始化,举个例子

int x;

在某些语境之下,x会被初始化为0。但在其他语境中x的值是不能被保证的。这就会导致不明确的行为,进行给你的程序带来未知的风险,这是需要避免的。未知的情况千变万化,但解决的办法显得“大道至简”——在你使用对象之前,将其初始化即可。

(1)内置数据类型

int x = 0;
const char* text = "A C-style string"; //手工初始化

(2)自定义数据类型

使用成员初始值列表来替换赋值操作:

class PhoneNumber{...};
class ABEntry{
public:
	ABEntry(const std::string &name,const std::string& address,const std::list<PhoneNumber>& phones);
private:
	std::string theName;
	std::string theAddress;
	std::list<PhoneNumber> thePhones;
	int num;
}

//赋值操作
ABEntry(const std::string &name,const std::string& address,const std::list<PhoneNumber>& phones)
{
	theName = name;
	theAddress = address;
	thePhones = phones;
	num = 0;				//赋值操作
}
/*赋值操作其实实施了2个步骤:
(1)调用default构造函数设立初值(default constructor)
(2)再对变量予以赋值(copy assignment)*/
//成员初始化值列表
ABEntry(const std::string &name,const std::string& address,const std::list<PhoneNumber>& phones)
:theName(name);
  theAddress(address);
  thePhones(phones);
  num(0;				//初始化操作
{	}		//构造函数不必有任何的动作

2个操作的结果是相同的,但是成员初值列的操作效率更高。赋值操作首先调用了default构造函数为其中的变量设立了初始值,然后立刻再对他们赋予新的数值。而成员初始化列表操作,只调用了一次copy构造函数是比较高效的。
需要注意的是,成员初始化列表的顺序是相对固定的,为了避免不必要的麻烦,初始化顺序可以遵循声明的次序。

(3)使用local_static对象替换none_local_static对象

书中说的比较晦涩,不同编译单元内定义之non-local static 对象的初始化次序,光看着就很头疼。首先要知道什么是local_static对象,什么是none_local_static对象。
在classes内部,函数内,file作用域内被声明为staic的对象被称为local_static对象;其他static对象被称为none_local_static对象。
而编译单元,指的是单一源码文件加上其所包含的头文件。而不同的编译单元,说明至少涉及2个编译单元;C++对于不同编译单元内的none_local_static对象的初始化次序并没有明确的定义。所以在程序运行过程中,某个编译单元内的none_local_static对象在初始化动作中使用了另一个编译单元内的某个none_local_static对象,他所用到的这个对象还没有初始化,这就带来了问题。
举个栗子:

编译单元A内:
class FileSystem{
public:
	...
	std:size_t numdisks() const;  //成员函数
	...
};
extern FileSystem tfs;
//预留给用户使用对象
 
编译单元B内:
class Directory
public:
{
	Directory(params);
	...
};
Directory::Directory(params){
	...
	std::size_t disks = tfs.numDisks(); //使用tfs对象
}

之前讲过C++对于不同编译单元内的none_local_static对象的初始化次序并没有明确的定义,所以上述程序运行时会出现不可预料的错误。既然无法保证初始化次序,能否明确初始化顺序来达到这一目的呢,C++目前没有机制可以保证这一点,无法做到。
解决方法:将none_local_static对象搬到一个专属函数内(该对象在函数内被声明为static),这些函数返回一个reference指向他所含的对象。类似于单例模式的手法。
在使用过程中,用户直接调用这些函数,而不是直接调用对象。

编译单元A内:
class FileSystem{...};
FileSystem& tfs(){
	static FileSystem fs;
	return fs;
}
 
编译单元B内:
class Directory{...};
Directory::Directory(params){
	...
	std::size_t disks = tfs().numDisks(); 
	...
}
Directory& tempDir(){
	static Directory td;
	return td;
}

这在单线程中是ok的,如果是在多线程系统中仍然有一定的不确定性,因为他在调用对象的过程中,在等待他进行初始化。这种情况是比较危险的,会有意想不到的错误发生。

4、总结

书山有路勤为径,学海无涯苦作舟。

5、参考

5.1 《Effective C++》

你可能感兴趣的:(#,《Effective,C++》,c++)