将C++看成一个语言联邦,C++有一些强烈的特性,为了方便理解,作者将其总结为次语言。
C++是以C为基础的,区块,语句,预处理器,内置数据类型,指针等诸多概念都是来自C。C++可以用来处理大型的复杂项目;而当前的C语言,主要用来编写51,32的MCU开发,底层驱动。
class,封装,继承,多态,虚函数(动态绑定等等),这一部分是面向对象的设计在C++上最直接的体现。
涉及泛型编程,内置数种可供套用的函数或者类。良好的编程守则“惟Templete适用”。
STL是个模板库,主要涉及容器,算法和迭代器
在不同情况下使用适合的部分,可以使 C++ 实现高效编程。
这个条款的本质:在编程过程中多用编译器,少用预处理器。因为#define是宏定义,他不被视作语言的一部分。而const,enums,inlines对应着3种不同的情况。
举个栗子:
#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"补偿做法。
一个属于枚举类型的数值,可以权充ints使用。
class GamePlayer{
private:
enum{NumTurns = 5};
int scores[NumTurns];
...
}
另一种情况就是实现宏,使用宏的时候,尤其要注意,表达式外要加上足够多的括号,不然,会有一些意想不到的错误。
举个栗子:
//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在现阶段是无法被代替的,要充分了解他,并知悉他潜在的风险。
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关键字在这里确保了函数不会进行任何可能影响程序状态
//的修改,并且使得函数的输出具有确定性。
首先要明确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。
为什么对象使用之前要进行初始化,举个例子
int x;
在某些语境之下,x会被初始化为0。但在其他语境中x的值是不能被保证的。这就会导致不明确的行为,进行给你的程序带来未知的风险,这是需要避免的。未知的情况千变万化,但解决的办法显得“大道至简”——在你使用对象之前,将其初始化即可。
int x = 0;
const char* text = "A C-style string"; //手工初始化
使用成员初始值列表来替换赋值操作:
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构造函数是比较高效的。
需要注意的是,成员初始化列表的顺序是相对固定的,为了避免不必要的麻烦,初始化顺序可以遵循声明的次序。
书中说的比较晦涩,不同编译单元内定义之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的,如果是在多线程系统中仍然有一定的不确定性,因为他在调用对象的过程中,在等待他进行初始化。这种情况是比较危险的,会有意想不到的错误发生。
书山有路勤为径,学海无涯苦作舟。
5.1 《Effective C++》