Effective C++学习------让自己习惯C++

柯河
2023-12-01

C++为一个语言联邦

1.概念:C++是一个面向对象的程序程序设计,那么对于面向对象,他是C语言的衍生版,所以它既可以进行C语言的过程化设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。

2.对于C++而言,它之所以被称之为语言联邦,是因为它有四个次语言,
如下:

  • C语言:由于C++语言是C语言的衍生出来的,以C语言为基础,所以具有C语言的一些行为,如:区块,语句,预处理,内置数据类型,数组,指针等都是来自C语言的。
  • 面向对象的C++:有类的操作,对于类成员,包含有构造函数和析构函数等;对于类的操作,有类的继承,多态,虚函数等。
  • 模板:在C++中的泛型编程部分。
  • STL:是一个程序库,里面包含了许多非常实用的东西,例如:容器,迭代器,算法,函数对象,并且里面的源码是非常的经典。

尽量用const,enum,inline代替#define

为什么说要用他们去替换掉#define呢?
1.首先,对于#define定义的一些常量,例如:#define ASPECT_RATIO 1.635定义了一个常量,在程序运行的时候,那么在进行预编译的时候,会直接将程序中的ASPECT_RATIO这个定义的常量直接用1.632替换掉了,并且这个常量的名称有可能连记号表的进不去,这样做是很可怕的,例如:当你用此变量得到一个编译时错误的信息时,他会提示的是1.632出现错误,如果写的很复杂的话,那么完全不知道错误出现在哪,连追踪都无法追踪。
而解决的时候可以通过const去解决,例如:

const double AspectRatio = 1.635;

这样作为一个语言常量,AspectRatio肯定会被编译器看到,并且也会进入记号表中,如果其出现问题的时候,可以直接定位。

2.其次,对于用const替换#define一般情况下有两个情况:

①: 定义常量指针:
对于定义常量指针,我们一般会通过string来定义,因为如果用char* 来定义的话对于const的定义会出现分期:

const char* authorName = "abc"//这个是对指针定义为常量,意思为“abc”是不能改变的,但是authorName指针变量可以指向别的地址。
char* const authorName = "abc"//为“abc”是可以改变的,但是authorName不可以指向别的地方。
const char* const authorName = "abc"//这是两者都不能改变,但是要写两次const

而如果使用的是string的话,直接写一次const即可,如下:

const std::string authorName("abc")//这样一来,写了一个const但是得到了两者都不能改变的情况。

②:class的专属常量,这个常量的作用域仅限制于这个类内。
如下进行定义:

class GamePlayer
{
 private:
  static const int NumTurns = 5;//这是声明式
  int scores[NumTurns];//这是定义式
};

因为其NumTurns是类成员中的static成员,所以他可以是声明式,对于他,可以在类中去使用,但是只要不去取它们的地址,就可以直接用声明式,不需要在给其定义以下,如果要取其地址,那么要在类外给其一个这样的定义:

const int GamePlayer::NumTurns;//是类外定义,由于其在类内已经初始化了,所以定义的时候不必初始化了。

对于的static成员变量,以及成员函数,它的作用域会延长。所以对于类中的其成员和函数,这些函数及成员变量只能访问静态的变量以及函数,不能访问非静态的成员和函数,而非静态的成员和函数可以访问其。并且在继承中,static的一系列成员都只有一份,不管如何继承,他们就只存在一份。

③:对于#define,他不能定义类的常量成员,因为对于#define,它不重视作用域,一旦宏被定义,那么在其后的编译过程中就有效,所以它不仅不能用来定义class的专属常量,而且也不能提供任何封装性,但是const成员可以。

3.对于2中还有一些问题:

对于一些类中的静态成员变量无法在类中被初始化的时候,那么对于二中的类的数组成员就无法创建,所以对于这种情况,外面一般是通过enum枚举类型的数值进行使用,如下:

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

对于enum的使用,要注意以下情况:
①:对于enum,他是没有地址的,取它的地址是违法的。(对于const 取其地址是可以的,而对于enum和#define定义的常量,取其地址是违法的)
②:实用。

4.将函数声明为inline内敛函数:
①:对于这个函数,他会将这个函数在被调用的地方被展开,就降低了调用函数这一部分带来的时间的浪费。
但是对于#define ,会出现一些错误,例如:

#define CALL_WITH_MAX(a,b) f((a)>(b) ? (a):(b))

对于上面这个宏定义,我们可以看出他是想取两个数的最大值的但是,对于以下不同的输入,它会有不同的答案:

int a = 5,b = 0;
CALL_WITH_MAX(++a,b);//a会累加两次
CALL_WITH_MAX(++a,b+10);//a会累加一次

而对于这样的错误,我们完全可以使用inlne函数取解决,因为我们对于其的需求都是一样的:

template<class T>
inline void CALL_WITH_MAX(const T& a,const T& b)
{
 f(a > b ? a : b);
}

此时我们调用其函数就不怕出现参数被多次求,因为传进来的参数就是已经被加过的,它会遵守作用域的规则。

尽量使用const

对于const,我们在上面也有所涉及,它是将一个数据或者函数定义为其提供一个常属性,这样可以让编译器帮助我们去看管这个不变的量,如果它出现改变,编译器就会首先不同意,来阻止这样的行为。

const用法很多,它不仅可以修饰类外的全局常量,也可也修饰类内的静态成员和非静态成员,还可以修饰函数。

1.对于const修饰指针的时候,const放在*号的左右位置不同,对应的指针和指针的解引用的结果是否可以改变是不同的,如:

const char* authorName = "abc"//这个是对指针定义为常量,意思为“abc”是不能改变的,但是authorName指针变量可以指向别的地址。
char* const authorName = "abc"//为“abc”是可以改变的,但是authorName不可以指向别的地方。
const char* const authorName = "abc"//这是两者都不能改变,但是要写两次const

我们来重看上面这个代码,现在const的作用就体现出来了。

2.对于const修饰的函数可以让函数出现的错误被编译器识别出来,从而降低我们做的一些不必要的错误,例如:

class Rational{}//这是一个类
const Rational operator*(const Rational& lhs,const Rational& rhs);//这是对类的一个重载*运算符

上面这个重载*运算的函数为什么要用const进行修饰呢,主要也是为了防止我们出现以下错误,因为对于这个重载函数,我们肯定会对最终结果不会进行其他的赋值操作,所以我们在进行如下:

Rational a,b,c;
a*b = c;

这个操作的时候,就会出问题,给a*b的结果又赋予了一个c,这是一个很奇怪的做法,我们完全可以直接重新定义一个类对象,用c赋值,不必要进行如此麻烦的事情,所以对我们来说,这个代码就有一点的不尽人意,更甚至是在对此比较的时候,例如:

if(a*b = c)

其实我们是想说的是if(a*b == c)这样的情况的,但是因为少写了一个等号,所以也有问题了,这个问题一般还无法通过编译器错误提醒得到,得步步调试找到问题,所以我们如果在函数的返回值前面加上const岂不是很好,直接让他的返回值无法进行改变,如果再次遇到前面这种情况的时候,那么就直接定位错误位置,岂不美哉。

const成员函数

1.对于类成员的函数,进行const的修饰会得到以下的情况:

  • ①:它会使const接口比较容易理解。可以让我们知道哪个函数可以修改对象的内容,而哪个函数不可以。
  • ②:它会使“操作const对象”成为可能。

为什么呢?因为对于C++而言,有重载函数,而且对于同一个函数,使用const修饰这个函数和不使用const修饰是两个函数,它俩是构成重载的,所以,例如在底层,他会对同一个函数进行不同的两种写法,分别有一个可以用const修饰,一个没有,我们可以根据自己的需求对其进行调用,例如:

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;
};

对于上面这个类成员对象进行操作的时候,如果我们实例化出类对象,而使用的是const成员对应的重载[]的函数进行操作,那么结果是不可以进行修改的,而使用非const成员的结果可以修改。(其中,使用const成员函数的时候,我们实例化出来的类对象,也应该是const修饰的,因为上面函数返回类型是引用,所以返回出来的值就是类中私有成员的text对应位置的字符)

2.bitwist const派和logical const派

  • bitwist const派认为:对于一个const修饰的成员函数,这个函数不能修改对象内的任意一个bit位。
  • logical const派认为:对于一个const修饰的成员函数,它可以修改对象内的bit位,但是必须是当数据量大的时候,客户察觉不到的时候进行修改。

①:但是对于bitwist const派的一些成员函数会出现以下这些缺陷,先看以下代码:

class CTextBlock
{
public:
  char& operator[](std::size_t position)const//这个声明是bitwise const声明
  {
      return pText[position];
  }
private:
  char* pText;
};

对于上面这个const声明,只是对这个类成员函数的this指针进行了const,所以说,在这个函数内,只要不改变类成员的数据就可以进行。
但是这个函数有个致命的弱点,就是如果我不在类成员函数中去改变pText,而在类外去改变,那么就会出现被修改,这显然就不符合bitwiste const的初衷了,例如:

const CTextBlock cctb("Hello");
char* PC = &cctb[0];
*PC = 'J';

如果进行上面的这个程序,那么pText还是被改变了。
从而也就出现了logical const派了。

②:logical const 派:由于他们认为,在数据高速缓存的时候,在用户无法知道的情况下修改bit位是可以容忍的,那么这就肯定会出现在const函数内去修改数据,那么就一定会被bitwise const派所不认同,这个时候我们只需要给在const函数内会被修改的数据在定义的时候加上关键词mutable即可,例如:

class CTextBlock
{
public:
  size_t length()const;
private:
  char* pText;
  mutable size_t textLength;   //最近一次计算文本的长度
  mutable bool lengthIsValid;  //查看当前文本长度是否有效
};
size_t CTextBlock::length()const
{
  if(!lengthIsValid)
  {
    textLength = strlen(pText); //如果说文本长度无效了,那么就需要改变,所以得进行修改
    lengthIsValid = true;
  }
}

3.对于相同作用的const和non-const成员函数的代码重复问题处理

面对许多的代码重复,可能会伴随的一系列的问题,例如:编译时间,维护,代码膨胀等等,我们写代码肯定想着写的代码越少,且也能完成同样的任务,所以对于类成员中的const和non-const成员函数的重复代码处理是个很重要的问题。

①:对于相同的作用的const和non-const成员函数,其实他们主要的不同点就是在于是否会修改类成员的数据,如果不修改,我们就使用const函数,如果修改,我们就使用non-const函数,这样会让机器帮我们去保护数据的安全,而他们大部分其他的行为都是相同的,例如边界检查了,检查数据的完整性了等等。这些代码的重复显得有点让人很不舒服了,所以我们可以通过以下情况去解决:

class TextBlock
{
public:
  const char& operator[](size_t position)const
  {
     //此处省略一些操作。
    return text[position];
  }
  char& operator[](size_t position)
  {
    return const_cast<char&>(static_cast<const TextBlock>(*this)[position]);
  }
};

对于上面的函数,const函数是正常的,去处理const函数该处理的事情,而non-const函数的操作却不一样,他会将传进来的this指针进行一个强制类型转换位const类型,然后调用const类型的同类型函数,然后在返回的时候,会对返回值进行一次解const的操作,其中:

static_cast<const TextBlock>(*this)//是强制将this指针进行常转换。
const_cast<char&>//是将返回来的字符进行解常化。

这样一来一回的操作,让这个函数的代码量减少了很多,而对于为什么只在non-const函数中去改变呢? 肯定是因为在non-const成员函数中,我们可以去随便修改数据(当然是可以修改),不会被限制,因为我们调用non-const函数的时候就想着数据会被修改,而const函数不可,他是常化的,我们修改的时候,编译器会首先不同意的。

确保对象在使用前已经被初始化

这个情况我们应该见的是比较多的,因为对于我们定义的一个数据,如果说这个数据没有被初始化,然后我们就去使用这个数据,对于一些语境,他会给这些未初始化的数据赋值为0;但是一般情况下,因为我们对这个数据没有进行初始化,但是对于另外一些数据,他会给这个数据赋予随机值,如果说,我们进行的数据量很大,而出现这种错误,他也不会报错,那么就会让我们陷入一个很尴尬的处境,所以说,我们在使用数据的时候,也要确保他被初始化后在进行处理。

1.对于类成员的初始化:
类成员的初始化一般会吧压力给到类的构造函数身上,但是对于构造函数的不同写法,会出现是对类成员进行初始化,还是进行赋值,且看以下代码:

class PhoneNumber{};
class  ABEntry
{
public:
  ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones)
  {
    theName = name;
    theAddress = address;
    thePhone = phones;
    numTimeConsulted = 0; 
  }
private:
  string theName;
  string theAddress;
  list<PhoneNumber> thePhone;
  int numTimeConsulted;
}

对于上述这个类的构造函数,其实他并不是真正的给类成员进行了初始化,对于theName,theAddress,thePhone其实这是一个赋值操作,而不是初始化操作,因为,对于一个类来说,他的外置对象的类成员进行初始化的时候,初始化会放在进入构造函数之前,会发生在这些成员的default构造函数(一个可以被调用而不带任何实参者,它要么没有参数,要么参数都是缺省的)被自动调用的时候(也就是进入构造函数的内容之前)。而对于内置类型数据,就是上述类中的numTimeComsulted这个成员,对于它,它可以在定义的时候直接给值,所以他不保证在你所看到的哪个赋值动作之前就获得初值。

所以,对于上面所说的,要对外置对象进行初始化,那么我们首先在其进入类构造函数的本体的时候,我们调用以下其的default构造函数就可以了,如下:

class PhoneNumber{};
class  ABEntry
{
public:
  ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones):theName(name),
    theAddress(address),
    thePhone(phones),
    numTimeConsulted(0) 
  {}
private:
  string theName;
  string theAddress;
  list<PhoneNumber> thePhone;
  int numTimeConsulted;
}

这样就可以了,但是值得注意的一点就是,在构造函数对数据进行初始化的时候,初始化的顺序是在类中定义时候的顺序,与构造函数后面的顺序无关。
而为什么我们要坚持使用初始化呢?
因为对于一个外置对象来说,我们定义了它,肯定会调用它的构造函数,那么我们肯定想在调用构造函数的时候,直接给定好它的值,而不是先构造函数之后,再进行赋值语句的拷贝函数,那么这样效率不就下降了。
但是对于内置类型,他们的构造和赋值的消耗没有多大差距,但是为了方便我们操作,我们一般也都在构造函数的时,直接在初值列(就是上述列类的构造函数名" : "后的内容)对其进行初始化。

2.不同单元的两个或者多个类进行操作的时候的初始化问题:

意思就是,当我们在一个类中去使用另一个类的实例化的对象去操作的时候,如果这个时候我们在类中使用的哪个类还没有被实例化出来,那么就悲剧了,我们使用一个未初始化的类对象去操作,那么这肯定会出错误。

而对于这种问题,我们只需要一个设计模式就可以解决了-----单例设计模式,
这个设计模式就是一个类只能实例化出一个对象,并提供一个访问它的全局访问点,这个实例并且被所有程序模块共享。但是这个实例化的对象要被static去修饰,因为,static对象,它的寿命会在被构造出来直到程序运行结束的时候停止,并且C++保证,函数内的本地静态对象会在这个函数被调用期间,第一次遇到该对象的定义式的时候被初始化,所以用函数调用(返回一个引用的方式)去替换直接访问非本地的静态对象(就是不是我这个单元的对象),所获得哪个引用就是经历了初始化的对象,并且,如果没有调用那个非本地的静态对象的话,那么也不用去承担其的构造和析构函数。
例如:

class FileSystem{};
FileSyetem& tfs() //用这个函数来替换其对象
{
  static FileSystem fs;
  return fs;
}
class Directory{};
Directory::Directory(params)
{
  size_t disks = tfs().numDisks();
}
Directory& tempDir() //用这个函数来替换其对象
{
  static Directory td;
  return td;
}

这样,我们对这两个类进行操作的时候,我们直接用tfs()和tempDir()这两个函数就可以。

 类似资料: