极客时间-cpp实战笔记(一)

一、cpp 的五种编程范式

  • 面向过程
  • 面向对象
  • 泛型编程
  • 模板元编程
  • 函数式(Lambda表达式)

二、预处理编程

  • 预处理指令都以符号“#”开头。虽然都在一个源文件里,但它不属于 C++ 语言,它走的是预处理器,不受 C++ 语法规则的约束。

  • 预处理编程也就不用太遵守 C++ 代码的风格。一般来说,预处理指令不应该受 C++ 代码缩进层次的影响,不管是在函数、类里,还是在 if、for 等语句里,永远是顶格写。

  • 可以使用 gcc 的 -E 选项。只输出预处理后的源码

  • #include 不仅可以包含头文件,可以包含任意的文件

    在写头文件的时候,为了防止代码被重复包含。#pragma once 是非标准实现,有的编译器不支持

    1
    2
    3
    4
    #ifndef _XXX_H_INCLUDED_
    #define _XXX_H_INCLUDED_
    ... // 头文件内容
    #endif // _XXX_H_INCLUDED_
  • 宏的展开、替换发生在预处理阶段,不涉及函数调用、参数传递、指针寻址,没有任何运行期的效率损失,所以对于一些调用频繁的小代码片段来说,用宏来封装的效果比 inline 关键字要更好,因为它真的是源码级别的无条件内联。

  • 宏是没有作用域概念的,永远是全局生效。所以,对于一些用来简化代码、起临时作用的宏,最好是用完后尽快用“#undef”取消定义,避免冲突的风险。或者,宏定义前先检查,如果之前有定义就先 undef,然后再重新定义

    1
    2
    3
    4
    #ifdef AUTH_PWD                  // 检查是否已经有宏定义
    # undef AUTH_PWD // 取消宏定义
    #endif // 宏定义检查结束
    #define AUTH_PWD "xxx" // 重新宏定义
  • 可以适当使用宏来定义代码中的常量,消除“魔术数字”“魔术字符串”(magic number)。

  • 使用命令 g++ -E -dM - < /dev/null 可以查看到编译器所支持的宏定义

三、编译阶段

1. 属性

对于编译期控制,c++11 之前标准没有规定,编译器进行了规定,gcc 中是 __attribute__,在 VC 中是 __declspec。从 c++11 之后,标准增加了”属性“ 这一概念来代替,用两对方括号的形式“[[…]]”,方括号的中间就是属性标签。

1
2
3
4
[[noreturn]]              // 属性标签
int func(bool flag) { // 函数绝不会返回任何值
throw std::runtime_error("XXX");
}

C++11 里只定义了两个属性:“noreturn” 和 “carries_dependency”。

c++14 增加了 deprecated,用来标记不推荐使用的变量、函数或者类,也就是被“废弃”。代码中使用了不推荐的变量或函数,会在编译的时候出现警告

属性支持非标准扩展,允许以类似名字空间的方式使用编译器自己的一些“非官方”属性,比如,GCC 的属性都在“gnu::”里。比如:

1
2
3
4
5
6
deprecated:与 C++14 相同,但可以用在 C++11 里。
unused:用于变量、类型、函数等,表示虽然暂时不用,但最好保留着,因为将来可能会用。
constructor:函数会在 main() 函数之前执行,效果有点像是全局对象的构造函数。
destructor:函数会在 main() 函数结束之后执行,有点像是全局对象的析构函数。
always_inline:要求编译器强制内联函数,作用比 inline 关键字更强。
hot:标记“热点”函数,要求编译器更积极地优化。

全部 gcc 属性参考:https://gcc.gnu.org/onlinedocs/gcc/Attribute-Syntax.html

1
2
[[gnu::unused]]      // 声明下面的变量暂不使用,不是错误
int nouse;

2. 动态断言和静态断言

assert 虽然是一个宏,但在预处理阶段不生效,而是在运行阶段才起作用,所以又叫“动态断言”。

静态断言叫做 static_assert,他是一个关键字,只在编译期间生效。

1
2
template <int N>
static_assert(N >= 0, "N >= 0");

四、面向对象

“面向对象编程”的关键点是“抽象”和“封装”,而“继承”“多态”并不是核心。建议在设计类的时候尽量少用继承和虚函数。

  • 特别的,如果完全没有继承关系,就可以让对象不必承受“父辈的重担”(父类成员、虚表等额外开销),轻装前行,更小更快。没有隐含的重用代码也会降低耦合度,让类更独立,更容易理解。
  • 还有,把“继承”切割出去之后,可以避免去记忆、实施那一大堆难懂的相关规则,比如 public/protected/private 继承方式的区别、多重继承、纯虚接口类、虚析构函数,还可以绕过动态转型、对象切片、函数重载等很多危险的陷阱,减少冗余代码,提高代码的健壮性。
  • 如果非要用继承不可,那么一定要控制继承的层次。如果继承深度超过 3 层,就说明有点“过度设计”了,需要考虑用组合关系替代继承关系,或者改用模板和泛型。
  • 在设计类接口的时候,我们也要让类尽量简单、“短小精悍”,只负责单一的功能。
  • C++11 新增了一个特殊的标识符“final”(注意,它不是关键字),把它用于类定义,就可以显式地禁用继承,防止其他人有意或者无意地产生派生类。无论是对人还是对编译器,效果都非常好,我建议你一定要积极使用。

1. c++ 中的函数

在现代 C++ 里,一个类总是会有六大基本函数:三个构造、两个赋值、一个析构。 3个构造: 构造函数、拷贝构造函数、转移构造函数。 2个赋值: 拷贝赋值函数、转移赋值函数。 1个析构: 析构函数。

对于这六大基本函数,可以使用 default、delete 关键字来告诉编译器,使用默认实现或者明确的禁用某个函数形式。

C++ 有隐式构造和隐式转型的规则,如果你的类里有单参数的构造函数,或者是转型操作符函数,为了防止意外的类型转换,保证安全,就要使用“explicit”将这些函数标记为“显式”。