【C++ Primer】第二章(2 ~ 6节)
阅读原文时间:2023年08月15日阅读:1

变量提供一个具名的、可供程序操作的存储空间。 C++变量对象一般可以互换使用。

变量定义(define)

  • 定义形式:类型说明符(type specifier) + 一个或多个变量名组成的列表。如int sum = 0, value, units_sold = 0;

  • 初始化(initialize):对象在创建时获得了一个特定的值。

    • 初始化不是赋值!
    • 初始化 = 创建变量 + 赋予初始值
    • 赋值 = 擦除对象的当前值 + 用新值代替
    • 列表初始化:使用花括号{},如int units_sold{0};

    注意:使用列表初始化时,如果初始值存在信息丢失的风险,编译器将报错:

    long double ld = 3.1415926536;
    int a{ld},b{ld}; //错误:转换未执行,因为存在丢失信息的危险
    int c(ld),d = ld; //正确:转换执行,丢失部分值
    • 默认初始化:定义时没有指定初始值会被默认初始化;在函数体内部的内置类型变量将不会被初始化

  • 建议初始化每一个内置类型的变量。

练习2.10:

变量的声明(declaration) vs 定义(define)

  • 为了支持分离式编译,C++将声明和定义区分开。声明使得名字为程序所知。定义负责创建与名字关联的实体。
  • extern:只是说明变量定义在其他地方。
  • 只声明而不定义: 在变量名前添加关键字 extern,如extern int i;。但如果包含了初始值,就变成了定义:extern double pi = 3.14;

  • 变量只能被定义一次,但是可以多次声明。定义只出现在一个文件中,其他文件使用该变量时需要对其声明。

  • 名字的作用域(namescope){}

    • 第一次使用变量时再定义它
    • main有全局作用域。

  • 嵌套的作用域

    • 同时存在全局和局部变量时,已定义局部变量的作用域中可用::reused显式访问全局变量reused。
    • 但是用到全局变量时,尽量不适用重名的局部变量。

练习2.11

变量命名规范

  1. 需体现实际意义
  2. 变量名用小写字母
  3. 自定义类名用大写字母开头:Sales_item
  4. 标识符由多个单词组成,中间须有明确区分:student_loan或studentLoan,不要用studentloan。
  • 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;

个人理解:左值是具有地址属性的对象。左值可以出现在=左边与=右边。

int i = 10; //i是左值
++i; //左值,地址为i的地址
  • 右值(r-value)只能出现在赋值语句的右边,比如常量。

个人理解:不是左值的对象就是右值。或者说无法操作地址的对象就叫做右值。右值只能出现在=右边。

int i2 = i + 1; // i + 1是一个临时对象,它有地址属性,但这个地址属性无法被使用,因此为右值
int i = 10;
i++; //右值,先返回一个临时变量,临时变量的地址无法被使用,可以视作没有地址属性

一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成;

复合类型的声明

练习2.25

说明下列变量的类型和值。

(a) int* ip, i, &r = i;
(b) int i, *ip = 0;
(c) int* ip, ip2;

解:

  • (a): ip 是一个指向 int 的指针, i 是一个 int, r 是 i 的引用。
  • (b): i 是 int , ip 是一个空指针。
  • (c): ip 是一个指向 int 的指针, ip2 是一个 int。

引用

一般说的引用是指的左值引用

  • 引用:引用是一个对象的别名,【引用类型】引用(refer to)另外一种类型。如int &refVal = val;
  • 引用必须初始化。
  • 引用和它的初始值是绑定bind在一起的,而不是拷贝。一旦定义就不能更改绑定为其他的对象

个人理解:引用是阉割版的指针,引用不是一个对象,而是给对象起的一个别名

引用定义时必须被初始化,并且与初始化的值绑定在一起,不像指针能够通过+或-运算符来指向其它内存地址;

练习2.15

下面的哪个定义是不合法的?为什么?

  • (a) int ival = 1.01;
  • (b) int &rval1 = 1.01;
  • (c) int &rval2 = ival;
  • (d) int &rval3;

解:

(b)和(d)不合法,(b)引用必须绑定在对象上而不是一个常量,(d)引用必须初始化。

练习2.16

考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了哪些操作?

int i = 0, &r1 = i;
double d = 0, &r2 = d;
  • (a) r2 = 3.14159;
  • (b) r2 = r1;
  • (c) i = r2;
  • (d) r1 = d;

解:

  • (a): 合法。给 d 赋值为 3.14159。
  • (b): 合法。会执行自动转换(int->double)。
  • (c): 合法。会发生小数截取。
  • (d): 合法。会发生小数截取。

指针

int *p; //指向int型对象的指针

  • 是一种 "指向(point to)"另外一种类型的复合类型。

  • 定义指针类型: int *ip1;从右向左读有助于阅读:1. ip1是指针;2. 一个指向int类型的指针。

  • 指针存放某个对象的地址

  • 引用不是对象,没有实际地址,因此不能定义指向引用的指针。

  • 获取对象的地址: int i=42; int *p = &i;&取地址符

  • 指针的类型与所指向的对象类型必须一致(均为同一类型int、double等)

  • 指针的值的四种状态:

    • 1.指向一个对象;

    • 2.指向紧邻对象的下一个位置;

    • 3.空指针;

    • 4.无效指针。

    • > 对无效指针的操作均会引发错误,第二种和第三种虽为有效的,但理论上是不被允许的

  • 指针访问对象: cout << *p;输出p指针所指对象的数据, *解引用符

  • 不能把int变量直接赋给指针

  • 空指针不指向任何对象。使用int *p=nullptr;来使用空指针。

  • 指针和引用的区别:引用本身并非一个对象,引用定义后就不能绑定到其他的对象了;指针并没有此限制,相当于变量一样使用。

  • 赋值语句永远改变的是左侧的对象。

  • void*指针可以存放任意对象的地址。能存,能赋值,但不能取:因无类型,仅操作内存空间,对所存对象无法访问。

  • 其他指针类型必须要与所指对象严格匹配

  • 两个指针相减的类型是ptrdiff_t

  • 建议:初始化所有指针。

  • int* p1, p2;//*是对p1的修饰,所以p2还是int型

  • 其他指针操作

练习2.21

请解释下述定义。在这些定义中有非法的吗?如果有,为什么?

int i = 0;
- (a) double* dp = &i;
- (b) int *ip = i;
- (c) int *p = &i;

解:

  • (a): 非法。不能将一个指向 double 的指针指向 int
  • (b): 非法。不能将 int 变量赋给指针。
  • (c): 合法。
练习2.23

给定指针 p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断的思路;如果不能,也请说明原因。

解:

能,可以使用try catch的异常处理来分辨指针p是否指向一个合法的对象,但通过普通控制结构无法实现。

练习2.24

在下面这段代码中为什么 p 合法而 lp 非法?

int i = 42;
void *p = &i;
long *lp = &i;

解:

void *是从C语言那里继承过来的,可以指向任何类型的对象。 而其他指针类型必须要与所指对象严格匹配。

  • 动机:希望定义一些不能被改变值的变量。

extern const

在C++中,extern const是用来声明外部链接的常量的关键字组合。

关键字extern用于声明一个变量或常量是在其他地方定义的,它告诉编译器该变量或常量的定义在其他文件中。这样,在当前文件中使用这个变量或常量时,编译器会在链接过程中找到它的实际定义。

关键字const表示该变量或常量是一个不可修改的值。它告诉编译器该标识符所表示的值在程序执行期间不会改变。

通过将extern const结合使用,我们可以声明一个外部链接的常量,该常量的定义位于其他文件中,并且在当前文件中不可修改。

例如,假设我们有两个文件:file1.cpp和file2.cpp。在file1.cpp中定义了一个常量:

    // file1.cpp
extern const int MY_CONSTANT = 10;

然后,在file2.cpp中可以使用这个常量:

// file2.cpp
extern const int MY_CONSTANT;

int main() {
    // 使用MY_CONSTANT
    int value = MY_CONSTANT;
    // ...
    return 0;
}

在这个例子中,extern const int MY_CONSTANT的声明告诉编译器,MY_CONSTANT的定义在其他文件中。在file2.cpp中使用MY_CONSTANT时,编译器会在链接过程中找到它的实际定义并使用该值。同时,由于使用了const关键字,MY_CONSTANT的值在程序执行期间是不可修改的。

初始化和const

  • const对象必须初始化,且不能被改变
  • const变量默认不能被其他文件访问,非要访问,必须在指定const定义之前加extern。要想在多个文件中使用const变量共享,定义和声明都加const关键字即可。

练习2.26

const int buf;      // 不合法, const 对象必须初始化
int cnt = 0;        // 合法
const int sz = cnt; // 合法
++cnt; ++sz;        // 不合法, const 对象不能被改变

const的引用

  • reference to const(对常量的引用):指向const对象的引用,如 const int ival=1; const int &refVal = ival;,可以读取但不能修改refVal。也就是底层const,因为const自带顶层const

  • 临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。

个人理解:临时量没有地址属性,也就是一个右值。但因为是常量引用,以后都不会修改temp的值,也就用不到temp的地址属性,因此这种赋值是合法的。

  • 对临时量的引用是非法行为。即当上图的ri不是常量引用时,赋值非法。

个人理解:因为普通的引用需要使用到被指变量的地址属性,而临时量是一个右值,没有地址属性,因此非法。

  • const引用可能引用一个并非const的对象

指针和const

  • pointer to const(指向常量的指针):不能用于改变其所指对象的值, 如 const double pi = 3.14; const double *cptr = &pi;
  • const pointer:指针本身是常量,也就是说指针固定指向该对象,(存放在指针中的地址不变,地址所对应的那个对象值可以修改)如 int i = 0; int *const ptr = &i;

练习2.27

下面的哪些初始化是合法的?请说明原因。

解:

int i = -1, &r = 0;         // 不合法, r 必须引用一个对象
int *const p2 = &i2;        // 合法,常量指针
const int i = -1, &r = 0;   // 合法
const int *const p3 = &i2;  // 合法
const int *p1 = &i2;        // 合法
const int &const r2;        // 不合法, r2 是引用, 引用自带顶层 const, 第二个const写法多余但合法, 但引用需要初始化.
const int i2 = i, &r = i;   // 合法
练习2.28

说明下面的这些定义是什么意思,挑出其中不合法的。

解:

//const修饰的变量必须初始化,无论是内置类型还是指针。

int i, *const cp;       // 不合法, const 指针必须初始化
int *p1, *const p2;     // 不合法, const 指针必须初始化
const int ic, &r = ic;  // 不合法, const int 必须初始化
const int *const p3;    // 不合法, const 指针必须初始化
const int *p;           // 合法. 一个指针,指向 const int
练习2.29

假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?请说明原因。

解:

i = ic;     // 合法, 常量赋值给普通变量
p1 = p3;    // 不合法, p3 是const指针不能赋值给普通指针
p1 = &ic;   // 不合法, 普通指针不能指向常量
p3 = &ic;   // 不合法, p3 是常量指针且指向常量, 故p3 不能被修改, 本句赋值语句正在修改
p2 = p1;    // 不合法, p2是常量指针, 有顶层const, 不能被修改
ic = *p3;   // 不合法, 对 p3 取值后是一个 int 然后赋值给 ic, 但ic是常量不能被修改

对于p1 = p3:
    int i = 0;
    const int *const p3 = &i;
    int *p1;
    p1 = p3;
错误出在第一个const上,因为编译器不知道p3指向的到底是常量还是变量,因此默认p3指向的是一个常量整数。所以p3不能乱赋值给一个非常量的int指针。
因此,编译器会报错,指出不能将const int *const类型的指针赋值给int*类型的指针,因为它们的常量性不匹配。

顶层const

  • 顶层const指针本身是个常量。
  • 底层const:指针指向的对象是个常量。拷贝时严格要求相同的底层const资格。

练习2.30

对于下面的这些语句,请说明对象被声明成了顶层const还是底层const?

const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;

解:

v2 是顶层const,p2 是底层const,p3 既是顶层const又是底层const,r2 是底层const。

练习2.31

假设已有上一个练习中所做的那些声明,则下面的哪些语句是合法的?请说明顶层const和底层const在每个例子中有何体现。

解:

r1 = v2; // 合法, 顶层const在拷贝时不受影响
p1 = p2; // 不合法, p2 是底层const,如果要拷贝必须要求 p1 也是底层const
p2 = p1; // 合法, int* 可以转换成const int*
p1 = p3; // 不合法, p3 是一个底层const,p1 不是
p2 = p3; // 合法, p2 和 p3 都是底层const,拷贝时忽略掉顶层const

constexpr和常量表达式(▲可选)

  • 常量表达式:指值不会改变,且在编译过程中就能得到计算结果的表达式。

字面值属于常量表达式,用字面值初始化的const对象也是常量表达式。

  • C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量的表达式。

    #include
    #include
    int main(void)
    {
    int i = 1;
    constexpr int &i2 = i; //报错,i不是常量表达式
    std::cout<<i2<<std::endl;
    i++;
    std::cout<<i2<<std::endl;
    return 0;
    }

    #include
    #include
    int main(void)
    {
    int i = 1;
    const int &i2 = i; //成功运行
    std::cout<<i2<<std::endl;
    i++;
    std::cout<<i2<<std::endl;
    return 0;
    }

  • 指针的constexpr

练习2.32

下面的代码是否合法?如果非法,请设法将其修改正确。

int null = 0, *p = null;

解:

非法,即使int的值恰好是0,也不能直接给指针赋值int变量。应改为

int null = 0, *p = &null;

而且应该注意到,null都是小写,并不是关键字或者预处理变量。

类型别名

  • 传统别名:使用typedef来定义类型的同义词。 typedef double wages;

  • 新标准别名:别名声明(alias declaration): using SI = Sales_item;(C++11)

    // 对于复合类型(指针等)不能代回原式来进行理解
    typedef char pstring; // pstring是char的别名
    const pstring cstr = 0; // 指向char的常量指针
    // 如改写为const char *cstr = 0;不正确,为指向const char的指针

    // 辅助理解(可代回后加括号)
    // const pstring cstr = 0;代回后const (char *) cstr = 0;
    // const char *cstr = 0;即为(const char *) cstr = 0;

auto类型说明符 c++11

  • auto类型说明符:让编译器自动推断类型

  • 一条声明语句只能有一个数据类型,所以一个auto声明多个变量时只能相同的变量类型(包括复杂类型&和*)。auto sz = 0, pi =3.14//错误

  • 会忽略引用:int i = 0, &r = i; auto a = r; 推断a的类型是int,因为r实际是指向i的,r只是i的一个别名。

    #include
    #include
    #include
    #include
    #include
    using boost::typeindex::type_id_with_cvr;

    int main()
    {
    int i = 100;
    const int& refI = i;
    auto i2 = refI;
    //输出int
    std::cout << type_id_with_cvr().pretty_name() << std::endl;
    return 0;
    }

  • 会忽略顶层const

auto关键字在推断类型时,如果没有引用符号,会忽略值类型的const修饰而保留修饰指向对象的const,典型的就是指针。

会忽略第二个const而保留第一个const

即会忽略顶层const,而保留底层const

即pi2的类型是const int *

理解:auto pi2 = pi;

​ 此时pi2与pi指向同一块地址,但当pi2发生变化,比如指向下一块地址时,不会影响到pi仍然指向原地址。所以const修饰符被忽略。

​ 而因为pi2有直接修改原地址(i=100所在的地址)中i的值的能力,为了防止pi2对const类型i进行修改,所以pi2的类型为const int*,即i为const

运行结果:const int *

  • const int ci = 1; auto f = ci;推断类型是int,如果希望是顶层const需要自己加constconst auto f = ci

个人理解:

auto f = ci; //值传递,f没能力改变ci的值,因此auto推断出来的类型不带const

const int ci = 1;
auto &f = ci;
std::cout << type_id_with_cvr<decltype(f)>().pretty_name() << std::endl; //此时则为const int &

练习2.35

判断下列定义推断出的类型是什么,然后编写程序进行验证。

const int i = 42;
auto j = i; const auto &k = i; auto *p = &i;
const auto j2 = i, &k2 = i;

解:

j 是 int,k 是 const int的引用,p 是const int *,j2 是const int,k2 是 const int 的引用。

decltype类型指示符

  • 表达式的类型推断出要定义的变量的类型。

9

  • decltype:选择并返回操作数的数据类型
  • decltype(f()) sum = x; 推断sum的类型是函数f的返回类型。
  • 不会忽略顶层const
  • 如果对变量加括号,编译器会将其认为是一个表达式,如int i-->(i),则decltype((i))得到结果为int&引用。
  • 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。
  • C++11

decltype和auto的区别

decltypeauto是C++11引入的两个类型推导机制,用于在编译时自动推导变量的类型。尽管它们可以实现类似的功能,但它们有以下区别:

  1. 推导的对象不同:

    • decltypedecltype表达式中推导出变量的类型。它可以获取变量、函数调用、表达式等的类型,包括修饰符和引用。
    • autoauto用于推导变量的初始值表达式的类型。它根据变量初始化的值来确定类型,并且通常会忽略顶层的const和引用修饰符。
  2. 推导时机不同:

    • decltypedecltype在编译时对表达式进行推导,因此可以用于推导尚未初始化的变量的类型。
    • autoauto在编译器遇到变量声明时进行推导,要求变量必须被初始化,以便从初始值推导出类型。
  3. 引用和顶层const的处理不同:

    • decltypedecltype保留变量的引用顶层const限定符。
    • autoauto推导的类型会去除引用和顶层const限定符,得到非引用类型。

下面是一些示例代码,用于展示decltypeauto的区别:

int x = 10;
const int& ref = x;

decltype(ref) a = x;  // 推导为 const int&
auto b = ref;         // 推导为 int

decltype(x + 1) c = x;  // 推导为 int
auto d = x + 1;         // 推导为 int

decltype((x)) e = x;    // 推导为 int&
auto f = (x);           // 推导为 int,去除了引用修饰符

decltype(x) g;          // 正确,推断为未初始化的int类型,decltype不需要初始化表达式
auto h;                 // 编译错误,auto 需要初始化表达式

综上所述,decltypeauto在类型推导时有一些区别:

  1. 表达式类型推导能力:

    • decltype能够推导出表达式的准确类型,包括修饰符和引用。
    • auto只能根据初始值的表达式推导出变量的类型,不考虑修饰符和引用。
  2. 引用和顶层const的处理:

    • decltype会保留变量的引用和顶层const限定符。
    • auto会去除引用和顶层const限定符。
  3. 推导时机:

    • decltype在编译时对表达式进行推导,可以用于推导尚未初始化的变量的类型。
    • auto在编译器遇到变量声明时进行推导,要求变量必须被初始化。
  4. 使用场景:

    • decltype通常用于需要获取表达式类型的情况,比如模板元编程或函数返回类型推导。
    • auto通常用于简化代码书写,尤其是在迭代器、范围循环等场景下,让编译器自动推导类型。

需要注意的是,由于auto是在编译时进行类型推导,因此它不能用于推导运行时动态类型的情况,例如函数参数的类型、函数返回类型无法使用auto进行推导。

练习2.36

练习2.37

struct

尽量不要把类定义和对象定义放在一起。如struct Student{} xiaoming,xiaofang;

  • 类可以以关键字struct开始,紧跟类名和类体。
  • 类数据成员:类体定义类的成员。
  • C++11:可以为类数据成员提供一个类内初始值(in-class initializer)。

编写自己的头文件

  • 头文件通常包含哪些只能被定义一次的实体:类、constconstexpr变量。

预处理器概述:

  • 预处理器(preprocessor):确保头文件多次包含仍能安全工作。

  • 当预处理器看到#include标记时,会用指定的头文件内容代替#include

  • 头文件保护符(header guard):头文件保护符依赖于预处理变量的状态:已定义和未定义。

    • #indef已定义时为真
    • #inndef未定义时为真
    • 头文件保护符的名称需要唯一,且保持全部大写。养成良好习惯,不论是否该头文件被包含,要加保护符。

    #ifndef SALES_DATA_H //SALES_DATA_H未定义时为真
    #define SALES_DATA_H
    strct Sale_data{

    }
    #endif

C++头文件保护符(Header Guard)是一种预处理指令,用于防止头文件被多次包含。当多个源文件包含同一个头文件时,头文件保护符确保头文件只会被编译一次,避免重复定义错误。

通常情况下,头文件保护符使用宏来实现。以下是一个常见的头文件保护符的示例:

#ifndef HEADER_NAME_H
#define HEADER_NAME_H

// 头文件内容

#endif

这里的HEADER_NAME_H是一个唯一的标识符,可以是任何合法的C++标识符。当编译器首次遇到#ifndef指令时,如果HEADER_NAME_H未定义,则继续编译头文件,并定义HEADER_NAME_H。如果HEADER_NAME_H已定义,则跳过头文件内容,避免重复编译。

头文件保护符的工作原理如下:

  1. #ifndef检查指定的标识符是否已定义。
  2. 如果标识符未定义(即第一次包含头文件),则执行#define指令来定义该标识符,并继续编译头文件内容。
  3. 如果标识符已定义(即头文件已经被包含过),则跳过头文件内容,避免重复编译。
  4. 最后,通过#endif指令结束头文件保护符的区域。

使用头文件保护符可以确保头文件只被编译一次,提高编译效率并避免重复定义错误。它是编写C++头文件时的常见做法。