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

相关推荐
Dream it possible!1 小时前
LeetCode 热题 100_在排序数组中查找元素的第一个和最后一个位置(65_34_中等_C++)(二分查找)(一次二分查找+挨个搜索;两次二分查找)
c++·算法·leetcode
柠石榴1 小时前
【练习】【回溯No.1】力扣 77. 组合
c++·算法·leetcode·回溯
王老师青少年编程1 小时前
【GESP C++八级考试考点详细解读】
数据结构·c++·算法·gesp·csp·信奥赛
澄澈天空2 小时前
C++ MFC添加RichEditControl控件后,程序启动失败
c++·mfc
Lzc7743 小时前
C++初阶——简单实现vector
c++·简单实现vector
一个小白14 小时前
C++——list模拟实现
开发语言·c++
程序员老舅4 小时前
C++ Qt项目教程:WebServer网络测试工具
c++·qt·测试工具·webserver·qt项目·qt项目实战
靡不有初1114 小时前
CCF-CSP第18次认证第一题——报数【两个与string相关的函数的使用】
c++·学习·ccfcsp
cookies_s_s6 小时前
Linux--进程(进程虚拟地址空间、页表、进程控制、实现简易shell)
linux·运维·服务器·数据结构·c++·算法·哈希算法
不想编程小谭6 小时前
力扣LeetCode: 2506 统计相似字符串对的数目
c++·算法·leetcode