C++编程实践--表达式与语句
文章目录
声明与定义
合理使用auto
使用auto可以避免编写冗长、重复的类型名,也可以保证定义变量时初始化。但是,auto类型推导规则复杂,需要仔细理解,在使用时应当确保推导的类型符合预期。
- 声明变量具有与函数调用的返回类型相同的类型
std::map<std::string, int>::iterator iter = m.find(val); auto iter = m.find(val); // 避免冗长的类型名
- 声明变量与非基础对象具有相同的类型
auto
类型推导时忽略引用,可能引入难以发现的性能问题:- 用
auto
初始化的变量,普通变量的const
修饰会忽略,而指针和引用的const
修饰会保留。
不要在接口头文件中用auto定义对外接口,例如:如果在头文件中使用auto定义了常量,可能在代码演进时因修改其值,而导致类型发生变化。
使用using定义类型别名
类型的别名实际是对类型的封装。而通过封装,可以让代码更清晰,同时在很大程度上避免类型变化带来的散弹式修改。
在C++11
之前,可以通过typedef
定义类型的别名:
typedef Type Alias; // Type 在前,还是 Alias 在前,不易理解
在C++11
之后,可以通过using定义类型别名:
using Alias = Type; // 符合'赋值'的用法,容易理解,不易出错
使用using定义模板的别名更简洁:
// 定义模板的别名
template <typename T>
using SpecialVector = std::vector<T, SpecialAllocator<T>>;
SpecialVector<int> data; // 使用别名
template <typename T>
class SomeClass {...
private:SpecialVector<int> data; // 模板类中使用别名
};
而typedef不支持带模板参数的别名,只能"曲线救国":
// 通过模板包装 typedef,需要实现一个模板类
template <typename T>
struct SpecialVector {typedef std::vector<T, SpecialAllocator<T>> type;
};
SpecialVector<int>::type data; // 使用别名时,额外多写 ::type
template <typename T>
class SomeClass {...
private:typename SpecialVector<int>::type data; // 需要加上 typename
};
禁止通过声明的方式引用外部函数接口和变量
只能通过包含头文件的方式使用其他模块或文件提供的接口。
通过声明的方式使用外部函数接口和变量,容易在外部接口改变时可能导致声明和定义不一致。同时,这种隐式依赖容易导致架构腐化。
【例外】
有些场景需要引用其内部函数,但并不想侵入代码时,可以通过 extern 声明方式引用。如:
针对某一内部函数进行单元测试时,可以通过 extern 声明来引用被测函数
当需要对某一函数进行打桩、打补丁处理时,允许 extern 声明该函数
禁止依赖不同编译单元内全局对象的初始化顺序
不同编译单元内全局对象的初始化顺序没有被严格定义,因此编写代码时不要依赖它们的初始化顺序,否则程序可能产生未定义行为。
变量初始化
确保对象在使用之前已被初始化。
遵循变量作用域最小化原则与就近声明原则,在变量被使用时才声明并初始化,使得代码更容易阅读,方便了解变量的类型和初始值。特别地,尽量使用初始化的方式对变量赋初值。
在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:
- 程序难以理解和维护:变量的定义与使用分离。
- 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(比如零)来初始化,使程序低效,如果变量在被赋于有效值以前被使用,还会导致错误。
【反例】
声明与初始化分离,降低了代码的可读性。
std::string name; // 声明时未初始化:调用默认构造函数
... // 在此期间未使用name变量
name = "nobody"; // 再次调用赋值操作符函数
...
【正例】
推迟变量定义,直到被使用时才定义并初始化。
std::string name{"nobody"}; // 调用构造函数初始化
...
优先使用{}初始化语法。
为了消除多种初始化语法带来的混乱,同时也覆盖到所有的初始化场景,C++11提供了{}
初始化语法。当确定不会进行窄化转换或者明确希望支持隐式转换时,可以使用=
初始化;当需要调用某个版本的构造函数时可以使用()
初始化;其他情况优先使用{}
初始化。
- 使用大括号
{}
初始化可以避免内置(built-in)类型之间的隐式窄化转换(narrowing conversions),而使用括号()
或等号=
初始化时不检查窄化转换
int a{1.2}; // 编译错误: 发生窄化
int b = 1.2; // 不符合:非预期的隐式类型转换
int a{5}; // 符合
int b = a; // 符合:确定不会发生窄化转换时使用赋值初始化
ArithmeticT c = 0; // 符合:明确需要该类型支持隐式转换
{}
初始化避免了C++语法解析引起的歧义
在C++中,任何可以被解释为声明语法的语句都会被解释为声明语句,这会导致调用默认构造函数创建对象的时候解析错误。
例如:
Foo a(); // 实际上声明了一个名字为 a 并且返回类型为 Foo 的函数
Foo b{}; // 可以用大括号默认构造对象
- 相对于C++提供的
=
和()
初始化,只有大括号{}
是可以在任何地方使用的,语法上简单通用
// 初始化结构体
struct A {int i;int j;
}