《Effective C++》
大约 6 分钟
《Effective C++》
参考
- 实体书,中文版,第三版,候捷
- 跟读视频
- [鲨哥带你读] Effective C++
- 《Effective C++》2023(上部完整版) C++进阶看这个,更详细,还有上机,但没更完
共有55个条款
让自己习惯C++
(01) 视C++为一个语言联邦
(02) 尽量以const, enum, inline 替换 #define
优先选择编译器而不是预处理器
预处理器的坏处
举例:
#define ASPECT_RATIO 1.653
发生错误时,编译器给出的错误信息是1.653,而不是ASPECT_RATIO,你可能难以定位,特别是目标代码中,可能出现多个1.653副本
C++11的常量表达式补充
另外,第三版时还没有C++11的constexpr,现在有更好的写法:
const double AspectRatio = 1.653;
另外需要区分多种"常量"
编译期常量 (Compile-time constants)
- 举例
- 数组:如果我们想要创建一个不是动态分配内存的数组,那么我们就必须给他设定一个size——这个size必须在编译期间就知道,因此静态数组的大小是编译期常量
- switch的case标签
- 模板
constexpr
#define
(倒不如说是预处理常量)
- 优点
- 安全、编译优化(快)
- 举例
运行期常量
举例
const (但有时可被优化成编译期常量)
// 因为他的值在编译期可能无法确定,例如: const int x = 10; // 编译期常量 int y; cin >> y; const int z = y; // 运行期常量
但需要注意的是,某些情况下,编译器可能会对运行期常量进行优化,将其视为编译期常量进行处理
两个特殊情况
常量指针
// 错误示例 // #define AUTHORNAME "11" const char* authorName = "11"; const char* authorName2 = "22"; authorName = authorName2; // 结果authorName的值变了 // 正常示例 const std::string authorName("11");
类的常量
class C { private: // #define NUMTURNS 5 // 宏无法作用域在类的内部 static const int NumTurns = 5; // 每个类都有一个常量副本 int scores[NumTurns]; // 这里可以在声明前进行定义 (神奇的特殊规则) } // 另一种写法是: class C { private: // #define NUMTURNS 5 // 宏无法作用域在类的内部 // static const int NumTurns = 5; // 每个类都有一个常量副本 enum { NumTurns = 5}; int scores[NumTurns]; // 这里可以在声明前进行定义 (神奇的特殊规则) }
函数宏 & 内联函数模板
缺点:容易被误用
例如
int f (int num) {return num;}
#define CALL_WITH_MAX (a,b) f((a)>(b)?(a):(b))
std::cout << CALL_WITH_MAX(++a, b) << std::endl; // 存在问题,++a会被执行两次
替代方案:内联函数模板
template<typename T>
inline T callWithMax(const T& a, const T& b) {
return f(a > b ? a : b);
}
总结
考虑到const、枚举、内联的可用性,对预处理器的需求减少了,但没有完全消除。
#include
仍然必不可少#ifdef/#ifndef
在控制编译方面发挥重要作用- 对于简单常量,首选const对象或枚举,而不是#define
- 对于类似函数的宏,优先选择内联函数
(03) 尽可能使用const
const指针
const用于指针时,用途非常广泛
const出现在
*
左侧:指向的是常量const出现在
*
右侧:指针本身是常量const在类型左右侧无区别
// 这两种写法等价: void f1(const Widget* pw); void f1(Widget const* pw);
总结
char* p = greeting; // 非常量指针、非常量数据 const char* p = greeting; // 非常量指针、常量数据 char const* p = greeting; // (上) char* const p = greeting; // 常量指针、非常量数据 const char* const p = greeting; // 常量指针、常量数据
个人记法:以星号为区分点,左侧是是指向类型,右侧是指针类型
((char)*) p = greeting; // 非常量指针、非常量数据 ((const char)*) p = greeting; // 非常量指针、常量数据 ((char const)*) p = greeting; // (上) ((char)* const) p = greeting; // 常量指针、非常量数据 ((const char)* const) p = greeting; // 常量指针、常量数据
const 迭代器
我觉得有点奇怪
例如
std::vector<int> vec;
// ...
// iter 的行为像 T* const
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; // 正确,非常量数据
++iter; // 错误,常量指针
// cIter 的行为像 const T*
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // 错误,常量数据
++cIter; // 正确,非常量指针
函数声明
const Rational operator*(const Rational& lhs, const Rational& rhs);
// operator*的结果应该是一个const对象,可以避免下面的写法:
(a*b) = c; // 错误写法
if (a*b=c); // 错误写法
const限定成员函数
能使接口更加明确
class TextBlock {
public:
const char& operator[](std::size_t position) const {} // 只读版,无法修改
char& operator[](std::size_t position) {} // 可写版,能够赋值
}
TextBlock tb("Hello");
std::cout<<tb[0]<<std::endl; // 调用non-const版
tb[0] = 'x'; // 正确
const TextBlock ctb("Hello");
std::cout<<ctb[0]<<std::endl; // 调用const版
ctb[0] = 'x'; // 错误
位常量与逻辑常量
成员函数成为const是什么意思?有两种流行概念:
- 位常量 (bitwise constness):也叫物理常量
- 逻辑常量 (logical constness):当你用一个特定的值创建一个常量对象,并且只调用 它的const成员函数时,你仍然改变它的值,这有些不对劲。这就引出了逻辑常量的概念
const成员函数不能修改它的对象的任何非静态数据成员
class CTextBlock {
public:
const char& operator[](std::size_t position) const {
a = 20; // 正确
b = "b"; // 错误
c = "c"; // 正确
}
private:
static int a = 10;
std::string b;
char c[100];
}
mutable (可变的) 将非静态数据成员从按位常量的限制中解放出来
class CTextBlock {
public:
std::size_t length() const {
b = std::strlen(text); // 正确
return b
}
private:
mutable std::size_t b;
mutable bool c; // 即让该值的位变化,视作 "其实没有修改类的内容",是安全的
}
避免const重载的代码重复