C++ Primer Notes(4): 变量初始化和作用域

这篇笔记是关于 C++ Primer 的 2.2 节 Variables 的记录和思考。

1. 变量是什么

变量是有名字的对象: variables are named objects.

那么什么是对象(object)? 一块具有类型的内存,称为对象。

而没有名字的对象, 则称为 unnamed objects, 或者说"匿名对象"。

2. 定义变量

指定类型,指定变量名字, 可选地给出初始化器。这就是变量定义。举例:

cpp 复制代码
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类型的对象则取值取决于类的定义:

  1. 没初始化的变量导致运行时问题
    如果变量没初始化,然后被用到,则容易导致难以排查的运行时错误;最佳实践是,对于 built-in 类型,始终初始化。

    作者狡辩说,编译器不需要检查没初始化的变量。呵呵。一边在推荐,另一边毫无作为,真的矛盾:
  2. malloc 或 new 的内存,如何初始化?
    这是我自己想到的,书上这一节压根没提。 在 OpenCV 中,cv::Mat() 的内部实现是调用了 fastMalloc() 函数,而 fastMalloc() 则是基于 malloc() 实现的,取值不确保是0.

这种做法在我看来是错误的,是不负责的,因为这把初始化的任务交给了用户,而用户并不都知道 cv::Mat() 的实现, 一旦用户忘记初始化, 得到的图像内容可能是有变化的,导致bug非常难排查。

正确做法应该是,申请图像像素内容时就做初始化, 考虑用 new 替代 malloc。 新的问题来了: new 的写法是怎样的?

cpp 复制代码
#include <iostream>

int main()
{
    int* data = new int[10 * 10]; // 结果不一定为0,是undefined行为
    //int* data = new int[10 * 10](); // 结果一定为0
    for (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 让它强制为别的值:

cpp 复制代码
g++ -fsanitize=address -g test13.cpp -o test13

然后修改初始化写法为第二种:

cpp 复制代码
int* data = new int[10 * 10]()

再次开启asan编译和运行,结果仍然是全0,说明是有效的初始化了的

参考: 从 -1094795586 到内存初始化

4. 变量声明

举例:

cpp 复制代码
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++来说,并不是。 看如下代码:

cpp 复制代码
#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 的概念, 应当精确到变量的生命周期, 而不是简单的划分到 {} 之间。

相关推荐
一只小灿灿10 小时前
C++ 修饰符全面详解
开发语言·c++
code_pgf11 小时前
C/C++ 中 `typedef` 关键字详解
c语言·c++
会周易的程序员11 小时前
js-shm: 高性能 Node.js 共享内存模块
开发语言·javascript·c++·node.js·共享内存·shm
2023自学中12 小时前
imx6ull 开发板 贪吃蛇, C++11 SDL2 无硬件GPU优化版
linux·c++
anscos12 小时前
为CUDA 代码引入静态分析
c++·工业软件·功能检测
众少成多积小致巨12 小时前
C++ 规范参考(中)
c++
小保CPP12 小时前
OpenCV C++基于AI模型的场景文本识别(OCR)
c++·人工智能·opencv·计算机视觉·ocr
shylyly_12 小时前
C++中的类型转换
开发语言·c++·匿名对象·隐式类型转换·拷贝优化
jinyishu_12 小时前
哈希表原理与开放定址法C++实现
c++·哈希算法·散列表
小龙报13 小时前
【优选算法】1. 水果成蓝 2.找到字符串中所有字母的异位词
java·c语言·数据结构·数据库·c++·redis·算法