【C++ Primer】第二章(2 ~ 6节)
变量
变量提供一个具名的、可供程序操作的存储空间。 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
变量命名规范
- 需体现实际意义
- 变量名用小写字母
- 自定义类名用大写字母开头:Sales_item
- 标识符由多个单词组成,中间须有明确区分: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语言那里继承过来的,可以指向任何类型的对象。 而其他指针类型必须要与所指对象严格匹配。
const限定符
- 动机:希望定义一些不能被改变值的变量。
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 = π
。 - 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 = ⁣ // 不合法, 普通指针不能指向常量
p3 = ⁣ // 不合法, 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<iostream>
#include<string>
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<iostream>
#include<string>
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 <iostream>
#include<stdio.h>
#include<string>
#include<sstream>
#include<boost/type_index.hpp>
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<decltype(i2)>().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需要自己加const
,const 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的区别
decltype
和auto
是C++11引入的两个类型推导机制,用于在编译时自动推导变量的类型。尽管它们可以实现类似的功能,但它们有以下区别:
-
推导的对象不同:
decltype
:decltype
从表达式中推导出变量的类型。它可以获取变量、函数调用、表达式等的类型,包括修饰符和引用。auto
:auto
用于推导变量的初始值表达式的类型。它根据变量初始化的值来确定类型,并且通常会忽略顶层的const
和引用修饰符。
-
推导时机不同:
decltype
:decltype
在编译时对表达式进行推导,因此可以用于推导尚未初始化的变量的类型。auto
:auto
在编译器遇到变量声明时进行推导,要求变量必须被初始化,以便从初始值推导出类型。
-
引用和顶层
const
的处理不同:decltype
:decltype
保留变量的引用和顶层const
限定符。auto
:auto
推导的类型会去除引用和顶层const
限定符,得到非引用类型。
下面是一些示例代码,用于展示decltype
和auto
的区别:
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 需要初始化表达式
综上所述,decltype
和auto
在类型推导时有一些区别:
-
表达式类型推导能力:
decltype
能够推导出表达式的准确类型,包括修饰符和引用。auto
只能根据初始值的表达式推导出变量的类型,不考虑修饰符和引用。
-
引用和顶层
const
的处理:decltype
会保留变量的引用和顶层const
限定符。auto
会去除引用和顶层const
限定符。
-
推导时机:
decltype
在编译时对表达式进行推导,可以用于推导尚未初始化的变量的类型。auto
在编译器遇到变量声明时进行推导,要求变量必须被初始化。
-
使用场景:
decltype
通常用于需要获取表达式类型的情况,比如模板元编程或函数返回类型推导。auto
通常用于简化代码书写,尤其是在迭代器、范围循环等场景下,让编译器自动推导类型。
需要注意的是,由于auto
是在编译时进行类型推导,因此它不能用于推导运行时动态类型的情况,例如函数参数的类型、函数返回类型无法使用auto
进行推导。
练习2.36
练习2.37
自定义数据结构
struct
尽量不要把类定义和对象定义放在一起。如
struct Student{} xiaoming,xiaofang;
- 类可以以关键字
struct
开始,紧跟类名和类体。 - 类数据成员:类体定义类的成员。
C++11
:可以为类数据成员提供一个类内初始值(in-class initializer)。
编写自己的头文件
- 头文件通常包含哪些只能被定义一次的实体:类、
const
和constexpr
变量。
预处理器概述:
- 预处理器(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
已定义,则跳过头文件内容,避免重复编译。
头文件保护符的工作原理如下:
#ifndef
检查指定的标识符是否已定义。- 如果标识符未定义(即第一次包含头文件),则执行
#define
指令来定义该标识符,并继续编译头文件内容。 - 如果标识符已定义(即头文件已经被包含过),则跳过头文件内容,避免重复编译。
- 最后,通过
#endif
指令结束头文件保护符的区域。
使用头文件保护符可以确保头文件只被编译一次,提高编译效率并避免重复定义错误。它是编写C++头文件时的常见做法。