辜老师的C++课堂笔记
  IaG2GcaSyaq1 2天前 15 0
C++

不代表全部内容

第二章 类型、常量及变量

2.1 C++的单词

单词包括常量、变量名、函数名、参数名、类型名、运算符、关键字等。
关键字也被称为保留字,不能用作变量名。
预定义类型如int等也被当作保留字

char16_tchar32_t是C++11引入的两种新的字符类型,用于表示特定大小的Unicode字符
例如 char16_t x = u'马';
wchar_t表示char16_t ,或char32_t
nullptr表示空指针

需要特别注意的是:char可以显示地声明为带符号的和无符号的。因此C++11标准规定char,signed char和unsigned char是三种不同的类型。
但每个具体的编译器实现中,char会表现为signed char和unsigned char中的一种。

unsigned char ua = ~0;
printf("%d  ", ua);//输出255
signed char ub = ~0;
printf("%d  ", ub);//输出-1
char uc = ~0;
printf("%d", uc);//输出-1

2.2 预定义类型(内置数据类型)及值域和常量

2.2.1 常见预定义类型

类型的字节数与硬件、操作系统、编译有关。假定VS2019采用X86编译模式。
void:字节数不定,常表示函数无参或无返回值。

void
是一个可以指向任意类型的指针类型。它本质上是一个
“无类型”*的指针,这意味着它可以指向任何类型的数据,而不关心具体的数据类型。

	int n = 0721;//前置0代表8进制
	double pi = 3.14;
	void* p = &n;
	cout << *(int*)p << endl;//不能直接解引用哦
	p = &pi;	
	cout << *(double*)p << endl;
  • bool:单字节布尔类型,取值false和true。
  • char:单字节有符号字符类型,取值-128~127。
  • short:两字节有符号整数类型,取值-32768~32767。
  • int:四字节有符号整数类型,取值-231~231-1 。
  • long:四字节有符号整数类型,取值-231~231-1 。
  • float:四字节有符号单精度浮点数类型,取值-1038~1038。
  • double:八字节有符号双精度浮点数类型,取值-10308~10308。
    注意:默认一般整数常量当作为int类型,浮点常量当作double类型。
  • char、short、int、long前可加unsigned表示无符号数。
  • long int等价于long;long long占用八字节。
  • 自动类型转换路径(数值表示范围从小到大):
    char→unsigned char→ short→unsigned short→ int→unsigned int→long→unsigned long→float→double→long double。
    数值零自动转换为布尔值false,数值非零转换为布尔值true。
  • 强制类型转换的格式为:
    (类型表达式) 数值表达式
  • 字符常量:‘A’,‘a’,‘9’,‘\’’(单引号),‘\’(斜线),‘\n’(换新行),‘\t’(制表符),‘\b’(退格)
  • 整型常量:9,04,0xA(int); 9U,04U,0xAU(unsigned int); 9L,04L,0xAL(long); 9UL, 04UL,0xAUL(unsigned long), 9LL,04LL,0xALL(long long);
    这里整型常量的类型相信大家看后面的字母也能看出来,例如L代表long,U代表unsigned

2.2.2预定义类型的数值输出格式化

  • double常量:0.9, 3., .3, 2E10, 2.E10, .2E10, -2.5E-10
  • char: %c; short, int: %d; long:%ld; 其中%开始的输出格式符称为占位符
  • 输出无符号数用u代替d(十进制),八进制数用o代替d,十六进制用x代替d
  • 整数表示宽度如printf(“%5c”, ‘A’)打印字符占5格(右对齐)。%-5d表示左对齐。
  • float:%f; double:%lf。float, double:%e科学计数。%g自动选宽度小的e或f。
  • 可对%f或%lf设定宽度和精度及对齐方式。“%-8.2f”表示左对齐、总宽度8(包括符号位和小数部分),其中精度为2位小数。
  • 字符串输出:%s。可设定宽度和对齐:printf(“%5s”,”abc”)。
  • 字符串常量的类型:指向只读字符的指针即const char *, 上述”abc“的类型。
  • 注意strlen(“abc”)=3,但要4个字节存储,最后存储字符‘\0’,表示串结束。

2.3 变量及其类型解析

2.3.1 变量的声明和定义(C++11标准3.1节)

  • 变量说明:描述变量的类型及名称,但没有初始化。可以说明多次
  • 变量定义:描述变量的类型及名称,同时进行初始化。只能定义一次
  • 说明例子:extern int x; extern int x; //变量可以说明多次
  • 定义例子:int x=3; extern int y=4; int z; //全局变量z的初始值为0
  • 模块静态变量:使用static在函数外部定义的变量,只在当前文件(模块)可用。可通过单目::访问。
  • 局部静态变量:使用static在函数内部定义的变量。
static int x, y; //模块静态变量x、y定义,默认初始值均为0
int main( ){    
    static int y;       //局部静态变量y定义, 初始值y=0
    return  ::y+x+y;//分别访问模块静态变量y,模块静态变量x,局部静态变量
} 

为了允许把程序拆分成多个逻辑部分来编写,C++支持分离式编译(separation compilation),即将程序分成多个文件,每个文件独立编译。
为了支持分离式编译,必须将声明(Declaration)定义(Definition)区分开来。声明是使得名字(Identifier,如变量名)为其它程序所知。而定义则负责创建与名字(Identifier)相关联的实体,如为一个变量分配内存单元。因此只有定义才会申请存储空间。
One definition rule(ODR):只能定义一次,但可以多次声明
如果想要声明一个变量而非定义它,就在前面加关键字extern,而且不要显示地初始化变量:

extern int i;   	//变量的声明
int i;//变量的定义(虽没有显示初始化,但会有初始值并分配内存单元存储,即使初始值随机)

任何包含显式初始化的声明成为定义。如果对extern的变量显式初始化,则extern的作用被抵消。
extern int i = 0; //变量的定义
声明和定义的区别非常重要,如果要在多个文件中使用同一个变量,就必须将声明和定义分离,变量的定义必须且只能出现在一个文件中,而其他用到该变量的文件必须对其声明,而不能定义。
例如,头文件里不要放定义,因为头文件可能会被到处include,导致定义多次出现。

值得一提的是C++17引入了 inline 变量,这允许在多个翻译单元中定义同一个全局变量而不引发重复定义的链接错误。inline 变量使得变量像 inline 函数一样,在多个文件中共享同一个定义。而在之前的标准中,inline只能用于函数而不能用于变量

  • 保留字inline用于定义函数外部变量或函数外部静态变量、类内部的静态数据成员。
  • inline函数外部变量的作用域和inline函数外部静态变量一样,都是局限于当前代码文件的,相当于默认加了static。
  • 用inline定义的变量可以使用任意表达式初始化
// header.h
inline int globalVar = 42;

关于其作用,可见C++17之 Inline变量

此外, C++20 引入了模块(Modules)机制,用于替代头文件的传统做法。模块减少了编译依赖并提高了编译速度。在模块中,变量声明和定义的规则更为清晰,模块能够很好地管理变量的可见性和作用范围。
如有兴趣,可见C++20 新特性: modules 及实现现状


2.3.2 变量的初始化(C++11标准8.5节)

  变量在创建时获得一个值,我们说这个变量被初始化了(initialized)。最常见的形式为:类型 变量名=表达式;//表达式的求值结果为变量的初始值
  用=来初始化的形式让人误以为初始化是赋值的一种。
  其实完全不同:初始化的含义是创建变量时设定一个初始值;而赋值的含义是将变量的当前值擦除,而以一个新值来替代。
实际上,C++定义了多种初始化的形式:

int a = 0;
int b = { 0 };
int c(0);
int d{ 0 };

  其中用{ }来初始化作为C++11的新标准一部分得到了全面应用(而以前只在一些场合使用,例如初始化结构变量)。这种初始化形式称为列表初始化(list initialization)

只读变量:使用constconstexpr说明或定义的变量,定义时必须同时初始化。当前程序只能读不能修改其值。constexpr变量必须用编译时可计算表达式初始化。
易变变量:使用volatile说明或定义的变量,可以后初始化。当前程序没有修改其值,但是变量的值变了。不排出其它程序修改。
const实例:extern const int x; const int x=3; //定义必须显式初始化x
volatile例: extern volatile int y; volatile int y; //可不显式初始化y,全局y=0
若y=0,语句if(y==2)是有意义的,因为易变变量y可以变为任何值。

  • volatile 的作用
    编译器通常会进行优化,将变量的值缓存到寄存器中,以提高访问速度。然而,某些情况下,变量的值可能会在程序执行过程中发生外部变化,例如通过硬件、信号、操作系统或多线程访问。因此,需要使用 volatile 关键字告知编译器,每次访问该变量时都要重新读取内存中的值,而不要使用优化的缓存值。

2.3.3 constexpr(c++11)

  **constexpr **是 C++11 引入的关键字,主要用于在编译期计算常量。constexpr 声明的变量或函数保证可以在编译时求值,并且在特定条件下也可以在运行时使用。它用于提高编译期计算的能力,从而优化程序的性能。

  • 任何被声明为 constexpr 的变量或对象,必须在编译时能求出其值。
  • constexpr 变量比 const 更加严格,所有的初始化表达式必须是常量表达式。

2.3.4 复合类型(C++11标准3.9.2)

  复合类型(Compound Type)是指基于其他类型定义的类型,例如指针引用。和内置数据类型变量一样,复合类型也是通过声明语句(declaration)来声明。一条声明语句由基本数据类型和跟在后面的声明符列表(declarator list)组成。每个声明符命名一个变量并指定该变量为与基本数据类型有关的某种类型。
复合类型的声明符基于基本数据类型得到更复杂的类型,如p,&p,分别代表指向基本数据类型变量的指针和指向基本数据类型变量的引用。,&是类型修饰符

在同一条声明语句中,基本数据类型只有一个,但声明符可以有多个且形式可以不同,即一条声明语句中可以声明不同类型的变量:
  int i, *p, &r; //i是int变量,p是int指针,r是int引用
正确理解了上面的定义,就不会对下面的声明语句造成误解:
  int * p1,p2; //p1是int指针,p2不是int指针,而是int变量
为了避免类似的误解,一个好的书写习惯是把类型修饰符和变量名放连一起:
  int *p1,p2, &r1;


2.3.5 指针及其类型理解

  • const默认与左边结合,左边没有东西则与右边结合

  • 指针类型的变量使用*说明和定义,例如:int x=0; int *y=&x;。

  • 指针变量y存放的是变量x的地址,&x表示获取x的地址运算,表示y指向x。

  • 指针变量y涉及两个实体:变量y本身,y指向的变量x。

  • 变量x、y的类型都可以使用const、volatile以及const volatile修饰。
      const int x=3; //不可修改x的值
      const int *y=&x; //可以修改y的值,但是y指向的const int实体不可修改
      const int *const z=&x; //不可修改z的值,且z指向的const int实体也不可改

  • 在一个类型表达式中,先解释优先级高的,若优先级相同,则按结合性解释。
    如:int *y[10][20]; 在y的左边是*,右边是[10],据表2.7知[ ]的优先级更高。
    解释: (1) y是一个10元素数组;(2)每个数组元素均为20元素数组
    (3) 20个元素中的每个元素均为指针int *

  • 但括号()可提高运算符的优先级,如:int (*z)[10][20];
    (…)、[10]、[20]的运算符优先级相同,按照结合性,应依次从左向右解释。
    因此z是一个指针,指向一个int型的二维数组,注意z与y的解释的不同。
    指针移动:y[m][n]+1移动到int 指针指向的下一整数,z+1移动到下一1020整数数组。


指针使用注意事项

  • 只读单元的指针(地址)不能赋给指向可写单元的指针变量。
    例如:
    const int x=3; const int *y=&x; //x是只读单元,y是x的地址
    int z=y; //错:y是指向只读单元的指针
    z=&x; //错:&x是是只读单元的地址
    证明:
    (1)假设int z=&x正确(应用反正法证明)
    (2)由于int z表示z指向的单元可写,故z=5是正确的
    (3)而
    z修改的实际是变量x的值,const int x规定x是不可写的。矛盾。
    可写单元的指针(地址)能赋给指向只读单元的指针变量: y=z;
    前例的const换成volatile或者const volatile,结论一样。
    int
    可以赋值给const int *,const int 不能赋值给int

  • 除了二种例外情况,指针类型类型都要与指向(绑定)的对象严格匹配:二种例外是:

  1. 指向常量的指针(如const int *)可以指向同类型非常量
  2. 父类指针指向子类对象

2.4 引用

引用(reference)为变量起了一个别名,引用类型变量的声明符用&来修饰变量名:

int  i = 10;//i为一个整型变量
int   &r = i; //定义一个引用变量r,引用了变量i,r是i的别名
//定义引用变量时必须马上初始化,即马上指明被引用的变量。
int   &r2;  //编译错误,引用必须初始化,即指定被引用变量

C++11增加了一种新的引用:右值引用(rvalue reference),课本上叫无址引用,用&&定义. 当采用术语引用时,我们约定都是指左值引用(lvalue reference),课本上叫有址引用,用&定义。关于右值引用,会在后续介绍

2.4.1 左值和右值

 C++表达式的求值结果要不是左值(lvaue),要不是右值(rvalue)。在C语言里,左值可以出现在赋值语句的左侧(当然也可以在右侧),右值只能出现在赋值语句的右侧。
 但是在C++中,情况就不是这样。C++中的左值与右值的区别在于是否可以寻址:可以寻址的对象(变量)是左值,不可以寻址的对象(变量)是右值。这里的可以寻址就是指是否可以用&运算符取对象(变量)的地址。

int i = 1;  	//i可以取地址,是左值;1不可以取地址,是右值
//3 = 4;		//错误:3是右值,不能出现在赋值语句左边
const int j = 10; 	//j是左值, j可以取地址
const int *p = &j;
// j = 20;	//错误:const左值(常量左值)不能出现在赋值语句左边

 非常量左值可以出现在赋值运算符的左边,其余的只能出现在右边。右值出现的地方都可以用左值替代。

 区分左值和右值的另一个原则就是:左值持久、右值短暂。左值具有持久的状态(取决于对象的生命周期),而右值要么是字面量,要么是在表达式求值过程中创建的临时对象。

//i++等价于用i作为实参调用下列函数
//第一个参数为引用x,引用实参,因此x = x + 1就是将实参+1;第二个int参数只是告诉编译器是后置++
int operator++(int &x, int)   { 
    int tmp= i; //先取i的值赋给temp
    x = x + 1;
    return tmp; 
}
//因此i++ = 1不成立,因为1是要赋值给函数的返回值,而函数返回后,tmp生命周期已经结束,不能赋值给tmp
//i++等价于operator++(i, int),实参i传递给形参x等价于int &x = i;

2.4.2 引用的本质

引用的本质还是指针,考查以下C++代码及其对应的汇编代码

int i = 10;
int &ri = i;
ri = 20;
//从汇编代码可以看到,引用变量ri里存放的就是i的地址

	int i = 10;
 mov         dword ptr [i],0Ah 	 //将文字常量10送入变量i 

	int &ri = i;
 lea         	eax,[i] 		//将变量i的地址送入寄存器eax
 mov         dword ptr [ri],eax  	//将寄存器的内容(也就是变量i的地址)送入变量ri

	ri = 20;
 mov         eax,dword ptr [ri]  	//将变量ri的值送入寄存器eax
 mov         dword ptr [eax],14h	//将数值20送入以eax的内容为地址的单元中

 引用变量在功能上等于一个常量指针
 但是,为了消除指针操作的风险(例如指针可以++,- -),引用变量ri的地址不能由程序员获取,更不允许改变ri的内容。
 由于引用本质上是常量指针,因此凡是指针能使用的地方,都可以用引用来替代,而且使用引用比指针更安全。例如Java、C#里面就取消了指针,全部用引用替代。

  •  引用与指针的区别是:
  1. 引用在逻辑上是“幽灵”,是不分配物理内存的,因此无法取得引用的地址,也不能定义引用的引用,也不能定义引用类型的数组。引用定义时必须初始化,一旦绑定到一个变量,绑定关系再也不变(常量指针一样)。
  2. 指针是分配物理内存的,可以取指针的地址,可以定义指针的指针(多级指针),指针在定义时无需初始化(但很危险)。对于非常量指针,可以被重新赋值(指向不同的对象,改变绑定关系),可以++, --(有越界风险)
  • 定义了引用后,对引用进行的所有操作实际上都是作用在与之绑定的对象之上。被引用的实体必须是分配内存的实体(能按字节寻址)
  1. 寄存器变量可被引用,因其可被编译为分配内存的自动变量。
  2. 位段成员不能被引用,计算机没有按位编址,而是按字节编址。注意有址引用被编译为指针,存放被引用实体内存地址。
  3. 引用变量不能被引用。对于int x; int &y=x; int &z=y; 并非表示z引用y, int &z=y表示z引用了y所引用的变量i。

例如

点击查看代码

struct A {
	int j : 4;     //j为位段成员
	int k;
} a;
void f() {
	int i = 10;
	int &ri = i;	//引用定义必须初始化,绑定被引用的变量
	ri = 20;		//实际是对i赋值20
	int *p = &ri;	//实际是取i的地址,p指向i,注意这不是取引用ri的地址
	//int &*p = &ri;  	//错误:不能声明指向引用的指针
	//int & &rri = ri;	//错误:不能定义引用的引用
	//int  &s[4];	//错误:数组元素不能为引用类型,否则数组空间逻辑为0

	register int i = 0, &j = i;	//正确:i、j都编译为(基于栈的)自动变量
	int  t[6], (&u)[6] = t;	//正确:有址引用u可引用分配内存的数组t
	int  &v = t[0];	//正确:有址引用变量v可引用分配内存的数组元素
	//int  &w = a.j; 	//错误:位段不能被有址引用,按字节编址才算有内存
	int  &x = a.k; 	//正确:a.k不是位段有内存
}

2.4.3 引用初始化

 引用初始化时,除了二种例外情况,引用类型都要与绑定的对象严格匹配:即必须是用求值结果类型相同的左值表达式来初始化。二种例外是:

  1. const引用
  2. 父类引用绑定到子类对象
点击查看代码
int j = 0;
const int c = 100;
double d = 3.14;
int &rj1 = j;		//用求值结果类型相同的左值表达式来初始化
//int &rj2 = j + 10;	//错误:j + 10是右值表达式
//int &rj3 = c;		//错误:c是左值,但类型是const int,类型不一致
//int &rj4 = j++;		//错误:j++是右值表达式
//int &rd = d;		//错误:d是double类型左值,类型不一致

 而const引用则是万金油,可以用类型相同(如类型不同,看编译器)的左值表达式和右值表达式来初始化

int j = 0;
const int c = 100;
double d = 3.14;
const int &cr1 = j;		//常量引用可以绑定非const左值
const int &cr2 = c;		//常量引用可以绑定const左值
const int &cr3 = j + 10;	//常量引用可以绑定右值
const int &cr4 = d;	//类型不一致,报**警告**错误	(VS2017)

int &&rr = 1;		//rr为右值引用
const int &cr5 = rr;	//常量引用可以绑定同类型右值引用

2.4.3.1 为什么非const引用不能用右值或不同类型的对象初始化?

 对不可寻址的右值或不同类型对象,编译器为了实现引用,必须生成一个临时(如常量)或不同类型的值,对象,引用实际上指向该临时对象,但用户不能通过引用访问。如当我们写
 double dval = 3.14;
 int &ri = dval;
 编译器将其转换成
 int temp = dval; //注意将dval转换成int类型
 int &ri = temp;
 如果我们给ri赋给新值,改变的是temp而不是dval。对用户来说,感觉赋值没有生效(这不是好事)。
 const引用不会暴露这个问题,因为它本来就是只读的。
 干脆禁止用右值或不同类型的变量来初始化非const引用比“允许这样做,但实际上不会生效”的方案好得多。

2.4.3.2 &定义的有址引用(左值引用)

 const和volatile有关指针的用法可推广至&定义的(左值)引用变量
 例如:“只读单元的指针(地址)不能赋给指向可写单元值的指针变量”推广至引用为“只读单元的引用不能初始化引用可写单元的引用变量”。如前所述,反之是成立的。
 int &可以赋值给const int &,const int &不能赋值给int &

  1.       const int &u=3;   //u是只读单元的引用
    
  2.       int &v=u;	          //错:u不能初始化引用可写单元的引用变量v
    
  3.       int x=3; int &y=x;//对:可进行y=4,则x=4。 
    
  4.       const int &z=y;    //对:不可进行z=4。但若y=5,则x=5, z=5。
    
  5.       volatile int &m=y;//对,m引用x。
    

2.4.3.3 &&定义的无址引用(右值引用)

右值引用:就是必须绑定到右值的引用。
 右值引用的重要性质:只能绑定到即将销毁的对象,包括字面量,表达式求值过程中创建的临时对象。
 返回非引用类型的函数、算术运算、布尔运算、位运算、后置++,后置--都生成右值,右值引用和const左值引用可以绑定到这些运算的结果上。
 c++ 11中的右值引用使用的修饰符是&&,如:
 int &&aa = 1; //实质上就是将不具名(匿名)变量取了个别名
 aa = 2; //可以。匿名变量1的生命周期本来应该在语句结束后马上结束,但是由于被右值引用变量引用,其生命期将与右值引用类型变量aa的生命期一样。这里aa的类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值

  1. &&定义右值引用变量,必须引用右值。如int &&x=2;
  2. 注意,以上x是右值引用(引用了右值),但它本身是左值,即可进行赋值:x=3;
  3. 但:const int &&y=2;//不可赋值: y=3;
  4. 同理:“右值引用共享被引用对象的“缓存”,本身不分配内存。”
int &&  *p;	//错:p不能指向没有内存的无址引用
int &&  &q;	//错:int &&没有内存,不能被q引用
int &  &&r;	//错:int &没有内存,不能被r引用。
int &&  &&s;	//错:int &&没有内存,不能被s引用
int &&t[4];    	//错:数组的元素不能为int &&:数组内存空间为0。
const int a[3]={1,2,3};   int(&& t)[3]=a; //错:a是有址的, 有名的均是有址的。&&不能引用有址的 
int(&& u)[3]= {1,2,3};    //正确,{1,2,3}是无址右值

右值引用的主要作用:

  1. 移动语义:允许临时对象的资源(如内存、文件句柄等)被“移动”到另一个对象中,而不是进行拷贝。这可以避免不必要的资源复制,提高程序效率。

  2. 完美转发:在模板编程中,能够将函数的参数以左值或右值的形式完美转发给另一个函数。

int b = 1;
//int && c = b; //编译错误! 右值引用不能引用左值

A getTemp() { return A( ); }
A o = getTemp();   // o是左值  getTemp()的返回值是右值(临时变量),被拷贝给o,会引起对象的拷贝

// getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了,而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量refO的生命期一样,只要refO还活着,该右值临时变量将会一直存活下去。
A && refO = getTemp();   //getTemp()的返回值是右值(临时变量),可以用右值引用,但不会引起对象的拷贝

//注意:这里refO的类型是右值引用类型(A &&),但是如果从左值和右值的角度区分它,它实际上是个左值(其生命周期取决于refO)。因为可以对它取地址,而且它还有名字,是一个已经命名的左值。因此
A *p = &refO;
//不能将一个右值引用绑定到一个右值引用类型的变量上
//A &&refOther = refO; //编译错误,refO是左值
  • 若函数不返回(左值)引用类型,则该函数调用的返回值是无址(右值)的
int &&x=printf(“abcdefg”);  //对:printf( )返回无址右值
     	int &&a=2;	//对:引用无址右值
     	int &&b=a; 	//错:a是有名有址的,a是左值
     	int&& f( ) { return 2; }
int &&c=f( );   	//对:f返回的是无址引用,是无址的
  • 位段成员是无址的。
struct A {   int a;	/*普通成员:有址*/     int b : 3; /*位段成员:无址*/ }p = { 1,2 };
int &&q=p.a;	//错:不能引用有址的变量,p.a是左值
int &&r=p.b;	//对:引用无址左值

2.5 枚举

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2天前 0

暂无评论

推荐阅读
  jk4wCss2qu8s   13天前   45   0   0 C++
  K9tJ5pQmM1gT   23小时前   10   0   0 C++
  K9tJ5pQmM1gT   3天前   19   0   0 C++