这篇笔记是关于 C++ Primer 的 2.2 节 Variables 的记录和思考。
1. 变量是什么
变量是有名字的对象: variables are named objects.
那么什么是对象(object)? 一块具有类型的内存,称为对象。
而没有名字的对象, 则称为 unnamed objects, 或者说“匿名对象”。
2. 定义变量
指定类型,指定变量名字, 可选地给出初始化器。这就是变量定义。举例:
int sum = 0, value, units_sold = 0;
Sales_item item;
std::string book("0-201-78345-X");
3. 变量初始化
1)初始化指的是定义变量的时候,一并获得了指定的值。
2)最基本的初始化,是 Type name=value
的形式。
3)C++11 新增了列表初始化: Type name{value}
, 推荐使用:
对于built-in 类型,这种初始化还能帮忙发现类型窄化(narrow)并报错编译错误:
4)当定义变量时没有指定初始化器,这就是“默认初始化”(default initialized)
- 对于built-in 类型,默认初始化指的是:
- 如果变量位于函数内,则没有初始化,取值是不确定的
- 如果变量位于函数外,则有初始化,取值是0
- 对于 class 类型,类的定义决定了使用这一类型定义变量时,是否需要指定初始化器。比如下面的 class A 就必须指定 initializer:
看到这里我确实体会到,C++ Primer不适合入门: 如果用户只有C语言经验,在不知道class的基本知识的情况下,是没法理解初始化器这里的说明的。所谓的 top-down 方式只不过是作者的一厢情愿罢了。
书上又特意强调一次:如果没有初始化,对于内置类型并且在函数内部那取值是不确定的,对于class类型的对象则取值取决于类的定义:
- 没初始化的变量导致运行时问题
如果变量没初始化,然后被用到,则容易导致难以排查的运行时错误;最佳实践是,对于 built-in 类型,始终初始化。
作者狡辩说,编译器不需要检查没初始化的变量。呵呵。一边在推荐,另一边毫无作为,真的矛盾:
- malloc 或 new 的内存,如何初始化?
这是我自己想到的,书上这一节压根没提。 在 OpenCV 中,cv::Mat() 的内部实现是调用了 fastMalloc() 函数,而 fastMalloc() 则是基于 malloc() 实现的,取值不确保是0.
这种做法在我看来是错误的,是不负责的,因为这把初始化的任务交给了用户,而用户并不都知道 cv::Mat() 的实现, 一旦用户忘记初始化, 得到的图像内容可能是有变化的,导致bug非常难排查。
正确做法应该是,申请图像像素内容时就做初始化, 考虑用 new 替代 malloc。 新的问题来了: new 的写法是怎样的?
#include <iostream>int main()
{int* data = new int[10 * 10]; // 结果不一定为0,是undefined行为//int* data = new int[10 * 10](); // 结果一定为0for (int i = 0; i < 10; i++){for (int j = 0; j < 10; j++){std::cout << data[i*10 + j] << ", ";}std::cout << std::endl;}delete[] data;return 0;
}
第一种写法, int* data = new int[10 * 10]
不一定产生全0结果,或者说,非常容易产生全0结果,但是不能总是保证是全0,它是编译器决定的。 那么为了让编译器不给我们使绊子,我们祭出 Address Sanitizer 让它强制为别的值:
g++ -fsanitize=address -g test13.cpp -o test13
然后修改初始化写法为第二种:
int* data = new int[10 * 10]()
再次开启asan编译和运行,结果仍然是全0,说明是有效的初始化了的
参考: 从 -1094795586 到内存初始化
4. 变量声明
举例:
int i; // 声明,并且定义变量 i
extern int j; // 声明,但是不定义变量 j
extern double pi = 3.1416; // 这是定义. 并且不能放在函数里
关于声明(declaration)和定义(definition)的详细区别,cppreference 给出了详细说明:
https://en.cppreference.com/w/cpp/language/definition
5. 作用域(Scope)
定义在函数之外的变量,叫做全局变量,拥有全局作用域(global scope)。
定义在函数内的变量,显然,也是定义在 {}
内的变量, 它的作用域叫做 block scope.
相对关系: 根据 scope 的大小,区分为 inner scope 和 outer scope。
作用域的概念说完了,接着说建议:
- 用到变量的时候再定义它
其实是应用了 “最小需要原则”。对于不需要这个变量的地方,比如外层block,或者其他函数, 则不需要让它们知道这个变量 - 避免shadowed variable
很遗憾,C/C++编译器默认不警告,更不报错。 -Werror=shadow 走起。
再说一些个人的补充思考:
书上提到的变量 scope 都是显示的, 其实还有隐式的scope。 啥意思呢? 就是说,当确定了一个 block (一个 curly brace, { }
), 变量的 scope 仍然是可以进一步确定边界的。 又或者说, 需要考虑变量的生命周期。 最需要考虑的有两个:
- return 语句
- throw 语句
return语句就是函数内最后执行的语句吗?
对于C++来说,并不是。 看如下代码:
#include <iostream>class A
{
public:A(const char* a_name): name(a_name){fprintf(stderr, "%s begin\n", name.c_str());}~A(){fprintf(stderr, "%s end\n", name.c_str());}
private:std::string name;
};int main()
{fprintf(stderr, "hello\n");A a1("a1");A a2("a2");fprintf(stderr, "good bye\n");return 0;
}
可以看到,return 语句之前执行的打印是 “good bye”,对应到红色 scope,但是 return 0 之后仍然有语句被执行,对应到黄色scope,再后来是另一条被执行的语句,对应到绿色scope。
显然在打印 a2 begin 和 a2 end 的时候, 红色的 scope 处于死亡状态。所以说, scope 的概念, 应当精确到变量的生命周期, 而不是简单的划分到 {
和 }
之间。