EC之Accustoming Yourself to C++

本文探讨了C++编程中的几个核心原则,包括将C++视为联合多种语言的集合、优选const和inline代替宏定义、充分利用const确保代码一致性及正确性、确保对象在使用前得到初始化等。这些实践有助于提升代码质量和程序性能。

条款01:View C++ as a federation of languages

我们可以把C++看成四部分,即C,Object-Oriented C++,Template C++,STL。

C是C++的基础,只不过C没有模板,没有异常,没有重载。

Object-Oriented C++则使C++拥有了面向对象设计的能力,类,封装,继承,多态,不一而足。

Template C++是C++的泛型编程部分,也是大部分程序员比较缺乏经验的部分。由于Templates巨大的威力,一种新的编程范型-模板元编程正在崭露头角。

STL则是一组紧密协作的组件,其中包括容器,迭代器,算法以及函数对象。

当你使用内置类型时,pass-by-value通常比pass-by-reference高效。当你使用用户自定义类型时,由于构造函数和析构函数的存在,pass-by-reference-to-const往往更好。使用Templates时同样如此,因为你不知道处理的对象的类型。而对于STL中的迭代器和函数对象来说,pass-by-value再次适用,因为它们是由指针塑造出来的。

因此,没有一成不变的编程法则,高效编程的策略取决于你使用C++的哪一部分。


条款02:Prefer consts,enums,and,inlines to #defines

用编译器代替预处理器能降低调试程序的难度。

现在,你定义了一个符号常量,#define COMPILER_NEVER_SEE_ME 1.6,但编译器并不知道它,因为预处理器早早地移走了它。当你获得一个由于该常量引起的编译错误信息时,信息里并不会提到COMPILER_NEVER_SEE_ME,只会提到让人迷惑的1.6。

较好的方法是使用常量替换宏:

const double CompilerCanSeeMeNow 1.6

有两种特殊情况值得一提:

第一,定义常量指针时,应该把const置于变量名之前,如int * const cpi = &i;定义了一个不变的指针指向i;

第二,在定义class专属常量时,如果在class编译期间需要一个class常量值,而你的编译器又不支持常量的“in class初值设定”,可以使用所谓的“the enum hack”,如:

class GamePlayer {
private:
	enum { NumTurns = 5 };

	int scores[NumTurns];
	...
};
其理论基础是枚举类型的数值可做int用。

基于数个理由,enum hack值得我们认识。

第一,enum hack的行为比较像#define而不像const,例如取#define和enum的地址都不合法,如果你不想让别人获得一个指针或引用指向你的某个整数常量,enum可以帮助你实现这个约束。而且enums和#defines一样绝不会导致不必要的内存分配。

第二,许多代码都使用了enum hack,事实上,它正是模板元编程(Template Metaprogramming)的基础技术。


定义带参数的宏可以免除函数调用的开销,但存在一些缺陷。考虑如下宏

#define max(A, B) ((A) > (B) ? (A) : (B))

语句max(i++, j++)将对i和j均执行两次自增操作,这显然和我们的本意相悖。

幸运的是,有了inline,你可以同时获得宏的效率和一般函数的可预料行为和类型安全性。可以将上述宏定义改写如下

template<typename T>
inline const T& max(const T& a, const T& b) {
	return a < b ? b : a;
}

总结:对于单纯的常量,尽量用const或enum替换#define;对于形似函数的宏,则最好用inline替换#define。


条款03:Use const whenever possible

const使编译器强制实施“对象不该被改变”的语义约束。


用const修饰变量时应该注意:

const出现在星号前面,表示被指物是常量;出现在星号之后,表示指针是常量。若被指物是常量,则const既可以放在类型之前,也可以放在类型之后,星号之前,如

const int *p等价于int const *p。

用const修饰迭代器,表示迭代器不可变。如果你希望迭代器所指物不可变,则需要使用const_iterator。


const最具威力的应用是在声明函数时。在函数声明中,const可以修饰返回值,参数以及函数自身(如果是成员函数)。

const成员函数使class接口容易被理解,同时使操作const对象成为可能(因为const对象、指向const对象的指针或引用只能用于调用const成员函数)。

如果两个成员函数只是常量性不同,可以被重载,这是被许多人漠视的C++一大特性。

现在我们来看看,如果成员函数是const意味什么。这里有两个流行概念:bitwise constness(也叫physical constness)和logical constness。

bitwise constness阵营的人认为,成员函数只有在不更改对象内的任一比特时才能被叫做const。这种论点的好处是容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内任何成员变量。

不幸的是,许多在概念上不具备const性质的成员函数仍可以通过bitwise测试。例如,一个更改了指针所指物的成员函数尽管在逻辑上不为const,但如果只有指针隶属于该对象,那么称此函数为const并不会引起编译器的异议。考虑以下类

class CTextBlock {
public:
  CTextBlock(std::string s) {
    const char *cpc = s.c_str();
    pText = new char[strlen(cpc) + 1];
    strcpy(pText, cpc);
  }
  char& operator[](std::size_t position) const {//bitwise const声明
    return pText[position];                     //其实不适当
  }
  ~CTextBlock() {
    delete []pText;
  }
private:
  char *pText;
};
显然operator[]是bitwise const,但看看它允许发生什么:

const CTextBlock cctb("hello");
char *pc = &cctb[0];
*pc = 'j'; //cctb的内容变为"jello"
这在逻辑上显然与常量性相悖。


这种情况导出了所谓的logical constness。这一派的拥护者主张,一个const成员函数可以修改对象内的某些bits,但只有在客户无法察觉的情况下才能如此。例如你的CTextBlock类可能缓存文本长度来应付查询:

class CTextBlock {
public:
  CTextBlock(const char *s):textLength(strlen(s)), lengthIsValid(true) {
    pText = new char[strlen(s) + 1];
    strcpy(pText, s);
  }
  std::size_t length() const {
    if (!lengthIsValid) {
      textLength = strlen(pText);
      lengthIsValid = true;
    }
    return textLength;
  }
  ~CTextBlock() {
    delete []pText;
  }
private:
  char *pText;
  mutable std::size_t textLength;     //最近一次计算的文本长度
  mutable bool lengthIsValid;         //目前的长度是否有效
};

length()方法并不符合所谓的bitwise constness,虽然它从概念上讲应该是const,但编译器不会同意我们的const声明,除非,我们使用mutable为成员变量除去bitwise constness的约束。mutable表示成员变量即使在const成员函数内也可能会被更改。

关于const的另一个难题是重载的const和non-const成员函数通常拥有相同的代码,如果代码较长,代码重复就是个问题。

我们可以用其中一个成员函数调用另一个成员函数,但是哪个调用哪个呢?

由于const成员函数承诺不改变对象的逻辑状态,而non-const版本则没有这样的保证,所以应该由后者调用前者才能保证安全。

为此,我们需要进行转型,先将当前对象添加const属性以调用const成员函数,再将返回值的const属性移除。其中,添加const属性使用static_cast,移除const属性则用const_cast。

class CTextBlock {
public:
  ...
  const char& operator[](std::size_t position) const {
    return pText[position];
  }
  char& operator[](std::size_t position) {
    return 
      const_cast<char&>(
	static_cast<const CTextBlock&>(*this)
	  [position]
      );
  }
  ...
private:
  char *pText;
};

通常转型不是一个较好的做法,但代码重复更让人烦恼,权衡之下,选择安全的转型还是可以接受的。


总结:

1.将对象声明为const可以帮助编译器检测错误。

2.编译器强制实施bitwise constness,但在编程时应该采用conceptual constness。

3.当const和non-const成员函数有着相同的实现时,令non-const版本调用const版本可以避免代码重复。


条款04:Make sure that objects are initialized before they're used

对于内置类型,C++不保证初始化,因此你必须手工初始化它们;而对于自定义类型,初始化的职责就落在了构造函数上。

值得注意的是,成员变量的初始化发生在进入构造函数本体之前的初始化阶段,即使不用初始化列表对成员变量进行显式初始化,构造函数也会调用各成员的default构造函数(对于内置类型,则不保证其初始化)。

我们可以用初始化列表来初始化各个成员变量,或者在构造函数本体中对成员进行赋值,但通常前者更高效。对于内置类型来说,初始化和赋值的成本相同;对于自定义类型,调用一次copy构造函数通常比调用一次default构造函数和一次copy assignment操作符高效。出于一致性的考虑,最好对所有成员都通过初始化列表进行初始化。值得一提的是,内置类型成员如果是const或reference,则必须使用初始化列表。初始化列表有时候绝对必要,且又比赋值高效,因此,总是使用初始化列表吧。

C++有着固定的“成员初始化次序”:base classes早于derived classes被初始化,而成员变量总是以其声明的次序被初始化。

下面我们来探讨下不同编译单元内定义的non-local static对象的初始化次序。

所谓static对象,其寿命从被构造出来直到程序结束为止。这种对象包括global对象、定义于namespace内的对象、在classes内、在函数内以及在file作用域内被声明的static对象。其中函数内的static对象被称为local static对象,其他static对象被称为non-local static对象。

所谓编译单元是指产生单个目标文件的源代码,基本上是一个源文件加上其所包含的头文件。

我们的问题涉及至少两个源文件,每一个至少包含一个non-local static对象。如果某编译单元内的non-local static对象的初始化使用了另一个编译单元的non-local static对象,但它所用到的对象可能并未被初始化,因为C++对定义于不同编译单元内的non-local static对象的初始化次序并无明确定义。



下载前可以先看下教程 https://pan.quark.cn/s/16a53f4bd595 小天才电话手表刷机教程 — 基础篇 我们将为您简单的介绍小天才电话手表新机型的简单刷机以及玩法,如adb工具的使用,magisk的刷入等等。 我们会确保您看完此教程后能够对Android系统有一个最基本的认识,以及能够成功通过magisk root您的手表,并安装您需要的第三方软件。 ADB Android Debug Bridge,简称,在android developer的adb文档中是这么描述它的: 是一种多功能命令行工具,可让您与设备进行通信。 该命令有助于各种设备操作,例如安装和调试应用程序。 提供对 Unix shell 的访问,您可以使用它在设备上运行各种命令。 它是一个客户端-服务器程序。 这听起来有些难以理解,因为您也没有必要去理解它,如果您对本文中的任何关键名词产生疑惑或兴趣,您都可以在搜索引擎中去搜索它,当然,我们会对其进行简单的解释:是一款在命令行中运行的,用于对Android设备进行调试的工具,并拥有比一般用户以及程序更高的权限,所以,我们可以使用它对Android设备进行最基本的调试操作。 而在小天才电话手表上启用它,您只需要这么做: - 打开拨号盘; - 输入; - 点按打开adb调试选项。 其次是电脑上的Android SDK Platform-Tools的安装,此工具是 Android SDK 的组件。 它包括与 Android 平台交互的工具,主要由和构成,如果您接触过Android开发,必然会使用到它,因为它包含在Android Studio等IDE中,当然,您可以独立下载,在下方选择对应的版本即可: - Download SDK Platform...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值