C++学习笔记(3):类型与变量
类型
类型决定了程序中数据和操作的意义,如
1 |
|
如果 i 和 j 的类型是数值,那么表达式就代表了数值相加运算,如果是其他对象就有了其他意义。
基本内置类型
C++的基本数据类型包括算术类型和空类型(void)。
算术类型
算术类型分为整型和浮点型两类。整型中包括了布尔型和字符型。
算术类型在不同机器上有不同的尺寸(size),并且:
- C++标准规定了它们的最小尺寸。
- C++允许编译器给予它们更大的尺寸。
这些类型的最小尺寸规定如下表:
类型 | 含义 | 尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整型 | 16位 |
int | 整型 | 16位 |
long | 长整型 | 32位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
其中:
- 布尔型的取值只有真(true)和假(false)。
- C++基本的字符类型是char,一个char的大小和一个机器字节一样,因为一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。其他字符类型用于扩展字符集:wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型char16_t和char32_t则为Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)。
除字符型和布尔型之外,其余的整型用于表示可能不同尺寸的整数,且C++有如下规定:
- int至少与short一样大。
- long至少与int一样大。
- long long至少与long一样大。
- long long是C++11标准中新定义的。
对于浮点数:
- 浮点型可表示单精度、双精度和扩展浮点型。
- C++规定了浮点数有效位的最小值,而编译器往往实现了更高的精度。
- 一般来说,float和double分别有7和16个有效位。
- long double常被用于有特殊计算需求的硬件,因此具体实现不同,精度也不同。
带符号类型和无符号类型
带符号和无符号的类型仅仅指整型。带符号的类型可表示正数、负数和0,而无符号的类型只能表示大于等于0的值。上文所述的整型都是带符号的,要指定无符号的类型则需要在类型名前加unsigned关键字。
特殊地,字符型实际有三种:char、signed char、unsigned char,三种的长度都为一个字节。
虽然字面上有三种,但实际上只有signed char和unsigned char两种,char则可能是这两种中的一种,具体是哪种取决于编译器。实际上,char是C标准中的未定义内容。
类型转换
(1)布尔型与其他类型的转换:其他类型的0值解释为布尔型的false,非0值解释为true;布尔型的false解释为其他类型的0值,true解释为1。
(2)浮点型与整型的转换:浮点型转换为整型,将直接舍去小数部分只保留整数部分;整型转换为浮点型,整型将直接成为浮点型的整数部分,小数部分填充0,但是整型范围大于浮点型范围的话,可能出现精度损失。如下面的例子:
1 |
|
1 |
|
(3)无符号与带符号类型的转换:有符号转无符号,结果相当于这个有符号数对该类无符号数所能表示的值的个数取模后的值,如unsigned char占8位,可表示256个值,超出0~255的值转换为unsigned char则对256取模;无符号转有符号数则是未定义行为,具体方式取决于编译器,此时程序可能正常工作,也可能崩溃,也可能产生无意义的数据。
(4)上述类型转换的规则总结来说就是,尽可能保持原来的存储值不变,而只改变解释这些值的方式。
(5)类型转换分为显式的和隐式的。显式类型转换需要使用类型转换运算符显式地指定,而隐式地类型转换则在两个不同类型的值进行运算时自动发生,规则是尽可能往类型尺寸大的类型转换。
字面值常量
(1)整型:
十进制整数直接书写,八进制整数以0开头,十六进制整数以0x开头,如:
十进制:20;八进制:024;十六进制:0x14。
整型字面值的数据类型由它的值和符号决定:
- 默认情况下,十进制字面值是带符号数。
- 十进制字面值的类型是int、long、long long中能容纳下该值并且尺寸最小的那个。
- 八进制和十六进制字面值是能容纳其数值的int、unsigned int、long、unsigned long、long long、unsigned long long中尺寸最小的一个。
- 如果一个字面值连与之关联的最大数据类型都放不下,会发生错误。
- 类型short没有默认对应的字面值。
- 此外,字面值的部分类型可以显式指定。
虽然整型字面值的类型默认是带符号的,但是严格来说十进制字面值不会是负数,如-2要作为字面值时,仅仅存储了2,负号将作为运算符号,不在字面值之内。
(2)浮点型:
浮点型的字面值可表示为以下几种:
- 小数:3.14159。
- 科学计数法:指数部分用e或E表示,如:3.14159E5、1e3。
- 整数或小数部分全0可省略,如:0. 和 .0。
(3)字符型和字符串:
字符型字面值由单引号包含的单个字符表示,如:'a'。
字符串字面值由双引号包含的一系列字符表示,如:"abc"。
字符串的字面值实际上是由字符组成的数组,字面值本身代表了数组的首地址。
字符串字面值可以分行书写,实际上,当两个紧邻的字符串字面值之间只有空格、换行符等空白符时,这两个字符串应看作同一个字符串。
一些特殊的无法表示在字符串字面值中的字符要用转义字符表示,转义字符被看作是单个字符。
转义字符 | 含义 | ASCII码值 |
---|---|---|
\a | 响铃符 | 7 |
\b | 退格符,将光标位置移到下一页开头 | 8 |
\f | 进纸符,将光标位置移到下—页开头 | 12 |
\n | 换行符,将光标位置移到下—行开头 | 10 |
\r | 回车符,将光标位置移到本行开头 | 13 |
\t | 水平制表符,光标跳到下一个Tab位置 | 9 |
\v | 垂直制表符 | 11 |
\' | 单引号 | 39 |
\" | 双引号 | 34 |
\\ | 单反斜杠 | 92 |
\? | 问号 | 63 |
\0 | 空字符 | 0 |
\ooo | 用1~3位八进制数ooo为码值所对应的字符 | ooo(八进制) |
\xhh | 用1~2位十六进制hh为码符所对应的字符 | hh(十六进制) |
(4)布尔型和指针型:
布尔型字面值:真为true,假为false。
指针型:nullptr。
(5)显式指定字面值的类型:
通过给字面值添加前缀或后缀,可显式地指定字面值的类型:
字符和字符串字面值:
前缀 | 类型 | 含义 |
---|---|---|
u | Unicode 16字符 | char16_t |
U | Unicode 32字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8(仅字符串) | char |
整型和浮点型字面值:
后缀 | 类型 |
---|---|
u或U | unsigned |
l或L | long |
ll或LL | long long |
f或F | float |
l或L | long double |
复合类型
复合类型是基于另一种类型实现的类型,C++中主要为引用和指针。
引用
引用实质是为变量起了一个别名,定义一个引用的方式如下所示:
1 |
|
由于引用的实质是别名,因此a和b实质就是同一个变量,基于b的操作都是在绑定的变量a上进行的,完全可以像使用a一样使用b。但是由于引用引用的是另一个对象,而引用只是别名不是对象,因此无法定义引用的引用(即引用不可传递)。
引用只能同类型引用,一个int型的引用只能绑定int型的变量。并且引用只能绑定对象,而不能绑定字面值或表达式的运算结果。
在引用被定义时,引用就绑定了目标变量,并且一旦绑定不可修改,因此定义引用必须初始化。
指针
指针指向另一个变量,它本身也是一个对象。指针和其他变量一样,在块作用域中未被初始化时它的值也是一个未知值。
定义指针的方式为:
1 |
|
其中&为取地址符,&a为变量a的地址。相反地,使用*p可访问指针指向的对象。
指针的类型也应和指向的变量类型相同。
对指针的初始化可初始化为空指针,有以下三种方式:
1 |
|
空指针也就是0值。其中nullptr为指针字面值,表示空指针,在C++11标准中引入。NULL则在标准库cstdlib中定义,要使用需要包含头文件。
指针的值实际是整数,但是不能用整型给指针赋值。
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解。该类指针只能与其他指针进行比较、作为函数的输入或输出、赋值给其他指针,而不能进行自增自减操作,也不能直接对其指向的对象进行操作。
多重指针和引用
指针是一种对象,因此可以有多重指针,多重指针在使用时需要层层解引用。
可以有指针的引用,也就是为指针指定了别名,但不可能有引用的引用。
变量
变量的定义
变量实际上是一块具有名称的操作空间,C++的每个变量都有其数据类型,数据类型决定着变量所占内存空间大小和布局方式、能存储的值的大小和能进行的操作方式。因此C++的变量定义格式为类型+名称,如:
1 |
|
在C++中,一般情况下,变量和对象可以视为同一个东西(面向对象的基本特性),或者说对象也是一种变量。
在变量定义时可以同时定义多个类型相同的对象,并可以选择为其中任意个变量赋予初值:
1 |
|
在这里book就是一个string类型的变量,也是一个string对象。
变量的初始化
初始化
如果在定义变量的时候为变量赋予了一个初始值,我们称这个变量被初始化了。但是要注意,虽然赋值和初始化都使用“=”来进行操作,但是初始化不是赋值,它们是两个完全不同的操作,在很多时候它们看起来是一样的,在很多编程语言中它们确实没有区别,但在C++中赋值操作和初始化是有区别的,例如:
1 |
|
是没有问题的,但是
1 |
|
就会报错。
此外在对象的初始化和赋值时,所进行的操作也是截然不同的,此处暂时按下不表。
列表初始化
C++中对于变量初始化有好几种形式,以下都是变量初始化的形式:
1 |
|
上述四种初始化都是合法的,其中后两种使用花括号的方式称为列表初始化,因为由花括号括起来的一系列值为列表,将列表中的值用来初始化变量即为列表初始化。
列表初始化用于C++内置类型的变量时有一个重要特点:如果列表初始化提供的值用来初始化变量时存在丢失信息的风险,那么编译器会报错,即:
1 |
|
中对a, b进行初始化会报错,因为从float向int转换会丢失信息,而对c, d初始化则不会报错,会进行强制类型转换,丢失掉小数信息。
关于列表初始化有更多关于对象的内容,也暂时按下不表。
默认初始化
如果定义变量不进行初始化,则可能进行了默认初始化。
(1)当该变量是内置类型时:
① 若该变量位于全局作用域,即所有函数之外,那么该变量会被初始化为0,即该变量的存储空间中所有位均被置0。要注意:使用static定义的静态变量和全局变量一样存放在程序的全局区,初始化方式和全局变量一样。
② 若该变量位于局部作用域,即函数内定义的变量,那么该变量不会被初始化,也就是为该变量分配一块存储空间时,不会对这片空间里的值进行任何处理,是无意义的随机值,因此内置类型的局部变量必须进行初始化或赋值之后才能使用,否则程序可能会报错。
(2)当该变量是自定义类的对象时:
每个类各自决定其对象的初始化方式,也规定了是否允许不经初始化就定义该对象,如果允许那么还要规定未经初始化时该对象的值到底是什么。
变量的声明
C++支持分离式编译,这样可以把程序拆分成若干个逻辑部分来编写,也就是允许程序被分成若干个文件,每个文件可以被独立编译。
为了使一个文件中的代码可使用另一个文件中定义的变量,C++支持使用extern关键字进行变量声明:
1 |
|
声明使得该变量为程序所知,告知程序该变量在另一个地方被定义了,使得该变量的作用域被拓展。不使用extern关键字则视为定义并声明。
如果使用extern关键字则不能为变量初始化了,一旦初始化,就会被视为变量定义。
一个变量可以被多次声明,但是只能有一次定义。
标识符
标识符的命名规则
- 只能以字母、数字和下划线组成,且不能以数字开头,长度没有限制,但是对大小写敏感。
- C++语言保留了一些关键字(或称保留字)供语言本身使用,不能将这些关键字作为用户定义的标识符。此外C++还为一些标准库保留了一些名字。
- 定义在函数体外的全局标识符不能以下划线开头。
- 标识符中不能出现连续两个下划线,也不能使用下划线紧连大写字母开头。
标识符的命名规范
变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性:
- 标识符要能体现实际含义。
- 变量名一般用小写字母,如index,不要使用Index或INDEX。
- 用户自定义的类名一般以大写字母开头,如 Sales_item。
- 如果标识符由多个单词组成,则单词间应有明显区分,如 student_loan或studentLoan,不要使用studentloan。
名字的作用域
作用域
变量的作用域是该变量有效的范围,在该作用域中,变量是“可见的”,可以被使用,在作用域外,相当于该变量不存在。
变量的作用域分为全局作用域和块作用域,全局作用域是在所有函数和块之外的作用域,块作用域是在代码块或函数块内的作用域。
作用域嵌套
作用域可以嵌套,分为外层作用域和内层作用域,外层作用域内的名字对内层作用域可见,但内层作用域的名字对外层不可见。
若内层作用域中定义了一个与外层作用域里重名的变量,那么在该内层作用域中无法访问外层作用域中的同名变量,默认为访问内层作用域中的变量(屏蔽机制)。
通常情况下,如果函数有可能用到某全局变量,则不能再定义一个同名的局部变量。
const限定符
使用const定义常量
C++中真正的常量只有字面值常量,而在实际使用中,我们希望使用一个名称代表常量,一般有两种方法:
- 使用宏定义表示常量
- 使用变量表示常量
我们希望常量的值是恒定无法修改的,在使用一个变量表示常量时,为防止意外的原因导致该常量的值被修改,可以使用const限定符来定义变量,使得该变量的值无法被修改。也因此,该变量必须在定义时初始化,如:
1 |
|
和其他变量一样,初始化可以在编译时,也可在运行时,如上为在编译时初始化,运行时初始化如下:
1 |
|
同样的,可以用非const变量的值初始化const变量。
与宏定义类似地,const变量的原理也是替换,但不是在预处理阶段的替换,而是在编译或运行时,遇到了引用const变量的地方,都会被替换为变量的值。这时就产生了一个问题:
const变量若在编译时被替换为值,那么编译器需要能够访问到该值,而多文件C++中每个源文件都是独立编译的,在某文件中若定义
1 |
|
那么在另一个文件中对该变量进行声明的话,是无法访问到该值的,因为该值被编译到了另一个文件的数据区,每个文件独立编译则出现了问题。如果另一个文件同样用到了这个变量,那么需要定义并初始化一个同名的变量才可以正常使用,那么就产生了重复定义的错误。为了解决这一问题,C++规定,const变量只在当前文件有效,这样每个文件中重复定义就不会违背“一次定义”的原则。
但是如果该const变量在运行时初始化的话,在每个文件中定义又会出现问题,如
1 |
|
中在每个文件中执行get_size函数得到的结果可能不同,但我们希望得到的是一个值不会改变的常量。为了使该变量实现文件间共享,可以在定义和声明时都使用extern关键字:
1 |
|
(注意与普通变量区别:普通变量在文件间共享,定义不需使用extern,只有声明需要)
const引用
可以把引用绑定到const对象上,这样的引用同样是const修饰的引用,被称为对常量的引用:
1 |
|
这样的引用只能用来访问绑定的对象,而不能用来修改。
一般的引用是无法将引用绑定到字面值或类型不一致的对象上的,如下列情况就是不合法的:
1 |
|
但对于常量引用来说,却是允许的,常量引用可以绑定到类型不一致的对象、字面值甚至是任意表达式上,如下情况都是合法的:
1 |
|
对于上述情况:
(1)若常量引用所引用的是字面值或表达式,实际上引用的是一块临时的存储空间,这块空间里存储了相应的字面值或表达式结果。
(2)若常量引用所引用的是类型不一致的对象,如:
1 |
|
实际上C++编译器的对这样的行为的实现过程是:
1 |
|
也就是使用了一块临时空间,将引用绑定到了这块临时空间上。
这样就可以看出,之所以这些对于普通引用不合法的方式对于常量引用合法,就是因为普通引用绑定一块临时空间是无意义的,我们将引用绑定到一个变量,当然是想要通过引用来操作这个变量,可是绑定到了一个临时空间上,最终并不能操作到想要绑定的变量,引用就失去了意义,而常量引用只使用其值,本就不应当对常量进行操作,所以引用实际是绑定到了一个临时空间还是绑定到了该变量,区别并不是很大,毕竟我们只是使用这个常量的值罢了,并不对其进行操作。
此外,const引用允许被绑定到一个非const变量上,但是如果这样做了,是不能通过该const引用来操作该变量的,但是可以通过其他方式来操作该变量。
const指针
指向常量的指针指向的变量可以是const的也可以是非const的,而普通指针不能指向const变量。指针和指向的对象的类型要一致,并且不能通过指向常量的指针来改变变量的值。其定义方式如下:
1 |
|
由于引用不是对象而指针是,因此可以把指针本身定义为const的,也就是常量指针。常量指针必须初始化,一旦指向了对象就不能再改变了,但是可以通过该指针去改变指向的变量的值。其定义方式如下:
1 |
|
总的来说,就是const关键字可以用来修饰类型(如const float),也可以用来修饰指针名。当修饰类型的时候,说明该指针指向的是一个const float对象,也就是指向常量的指针;而修饰指针名的时候,则说明该指针本身是const的,也就是常量指针。
那么也可以考虑把两者结合起来,定义一个指向常量的常量指针:
1 |
|
这样的话指针p指向常量,不能通过它修改pi的值,其本身也是常量指针,因此也不能改变指针指向的对象。
所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不允许我们去改变所指对象的值。
顶层const
如指针本身就是一个对象,它又能指向另一个对象,就好像指向的对象是指针的下一层一样,因此我们说指针在顶层,它指向的对象在底层,那么常量指针就是顶层const,指向常量的指针就是底层const。其实理解了const的原理就不必过多纠结顶层const和底层const的概念,这里仅作例子进行一些说明:
1 |
|
总之只需要注意一个原则:一般来说,变量可以转换为常量,反之则不可以。
常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。显然,字面值就属于常量表达式,使用常量表达式初始化的const对象也是常量表达式。
如下变量sz就不是一个常量表达式:
1 |
|
因为get_size函数值不能在编译过程中确定,其实是这个函数不是constexpr函数,如果它是constexpr函数那么sz就是常量表达式了。
constexpr变量
在实际使用中,我们希望一个变量是常量表达式,但是很多情况下我们很难发现一个初始值是不是常量表达式,因此C++11规定了一种新类型constexpr,声明为constexpr的一定是一个常量,并且必须用常量表达式来初始化,这在编译阶段会由编译器检查。如:
1 |
|
最后一个sz变量的定义要想不出错,size函数必须是constexpr函数。
C++11标准允许定义一种constexpr函数,这种函数必须简单到在编译阶段就能确定其值,这样就能用来初始化constexpr变量了。
只有能用字面值表示的类型才能定义为constexpr类型,特殊地,指针定义为constexpr类型只能初始化为空指针(0或nullptr)或是存储于某个固定地址中的对象。
constexpr限定符只对指针有效,而对其所指的对象无关,所以下面两条语句的效果相差甚远:
1 |
|
其实就是constexpr指针对标于常量指针,相当于顶层const,与其他常量指针类似,constexpr指针既可指向一个常量,也可指向一个非常量。
对于修饰Object来说,const并未区分出编译期常量和运行期常量,而constexpr限定在了编译期常量。
constexpr修饰的函数,返回值不一定是编译期常量。#It is not a bug, it is a feature.#
具体可见蓝色 的知乎回答
问题:指针可以指向constexpr变量吗?
处理类型和变量
类型别名
两种类型别名的定义方法:
1 |
|
都可以将SI定义为sales_item的别名,后一种是C++11中的规定。
对于指针类型也可以定义别名,如:
1 |
|
则pstring是char *的别名,但是要注意,此时pstring实际上是一种指针类型,在理解其本质时要认识到其指针类型的本质,而不是简单地替换,比如
1 |
|
并不等同于
1 |
|
前者是对pstring这种指针类型进行const限定,也就是常量指针,而后者是指向常量的指针。因此不能把类型别名理解为简单的替换。
auto类型说明符
使用auto类型说明符(C++11引入)可以在定义变量时不提供变量类型,而是由编译器自行判断,因此auto类型的变量必须初始化,如:
1 |
|
使用auto也能在一条语句中定义多个变量,但是注意一条语句中定义的变量只能是同一个类型,如:
1 |
|
此外,auto类型说明符使用时有以下特点:
- 使用引用来初始化auto对象时,auto对象的类型会被初始化为所引用对象的类型。
- 顶层const会被忽略,底层const会被保留。如果希望保留顶层const,则可以使用const auto。
decltype类型指示符
C++11还引入了decltype类型指示符,它可以返回操作数的数据类型,在此过程中仅仅是推断数据类型,而不计算结果。如在
1 |
|
中,sum的类型就是函数f的返回类型,在此过程中并不会实际调用f函数。
decltype处理顶层const和引用的方式与auto有些许不同:如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。引用从来都作为其所指对象的同义词出现,只有用在 decltype处是一个例外。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。有些表达式将向decltype返回一个引用类型,一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值。
现有如下例子:
1 |
|
对于上述例子:
- r是一个引用,因此decltype(r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是一个具体值而非一个引用。
- 如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)的结果类型就是int&,而非int。
- 如果对一个变量使用decltype,那么将得到该变量的类型,如果给变量加上一对括号,将得到该变量的引用类型。因为给变量加上括号,就成了表达式,而变量作为表达式是可以成为左值的。
自定义数据结构
自定义的数据结构可以通过结构体struct实现。
struct实际是C语言的特性,C++是面向对象的语言的同时还要向下兼容C语言,因此C++仍然保留了struct的写法,但实际定义的是类而不是简单的结构体了。
使用struct定义类的方法如下:
1 |
|
想要定义属于自己的数据结构,可大概遵从以下步骤:
- 定义类及其包含的成员变量。
- 为类定义别名。
- 将类定义写入头文件中,文件名同类名。
- 为源文件添加“ifndef - define - endif”编译控制来防止重复引入头文件。
- 在源文件中包含头文件并使用类。
C++11允许类为成员变量提供类内初始值,无外部初始化时使用默认初始值初始化成员变量。