基本内置类型
1.1 算术类型
算术类型分为:整型、浮点型。
bool类型和char类型规定为整型类型。
类型 | 含义 | 最小尺寸 |
---|---|---|
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位有效数字 |
计算机以2的整数次幂个比特序列来存储数据,其中可寻址的最小的内存块被称为“字节”,存储的基本单位称为“字”,一个字节由8个比特构成,字由4或8个字节构成。
除了布尔型和扩展的字符型之外,整型被分为signed 与 unsigned ,signed 类型可以表示负数,但 unsigned 类型只能表示大于等于0的值。
在类型名前添加unsigned就可以得到无符号类型。
特别注意,字符型被分为三种,char、signed char 和 unsigned char。char和signed char 并不一样,但是字符的表现形式只有 带符号 与 不带符号的,char类型只会表现为上述两种形式的一种,由编译器来决定。
在明确没有负数时,使用 unsigned 类型,能够节省空间。
1.2类型转换
对象的类型定义了对象所能包含的数据和能参与的运算。
常用的类型转换:
非布尔类型的算术值赋给布尔类型时,初始值大于0则结果为true。
布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1.
浮点数赋给整数类型时,结果值将保留浮点数中小数点之前的部分。
当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。(C++中,把负值赋给unsigned 对象是完全合法的,在原文中,作者把-1赋给8位的unsigned char,那么结果是255,因为255是-1对256求模后的值。)
赋给带符号类型一个超出它表示范围的值时,结果时未定义的,程序可能会崩溃。
当在程序中使用一种算术类型的值,而其实需要的是另一种类型的值时,编译器会自动进行类型转换,如果我们把一个布尔值用在算术表达式里,它的取值非0即1。
如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果(机器会在计算前把带符号类型会自动地转换成无符号类型,导致出现异常),一般不建议混合使用带符号类型和无符号类型。
1.3 字面值常量
一个形如42的值被称为字面值常量,每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整型字面值有十进制数、八进制数和十六进制数的形式。
(以0开头的整数代表八进制数,以0x或ox开头的代表十六进制数)
整型字面值的具体类型由它的值和符号决定的,默认情况下,十进制字面值是带符号的,八进制和十六进制两种情况都有可能。
浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识。
字符串字面值的类型实际上是由常量字符构成的数组,每个字符串的结尾有一个空字符 ' \0 '。
由单引号括起来的的一个字符称为char型字面值,双引号括起来的零个或多个字符构成字符串型字面值。
转义序列是用来打印一些不可打印的字符(无可视的控制字符)或含有特殊含义的字符(问号、反斜线、引号)。
换行符 | \n | 横向制表符 | \t | 响铃符 | \a |
纵向制表符 | \v | 退格符 | \b | 双引号 | \" |
反斜线 | \\ | 问号 | \? | 单引号 | \' |
回车符 | \r | 进纸符 | \f |
在程序中,上述转义序列被当作一个字符来使用。
泛化的转义序列:形式是\x 后紧跟1个或多个十六进制数字,或者1~3个八进制数字。
\7 | 响铃符 | \12 | 换行符 | \40 | 空格 |
\0 | 空字符 | \115 | 字符M | \x4d | 字符M |
注意,如果反斜线\后面跟着的八进制数值超过3个,只有前3个数字与 \ 构成转义序列。
我们可以通过添加前缀或后缀,去改变整型、浮点型和字符型字面值的默认类型。
前缀 | 含义 | 类型 |
u | Unicode16 字符 | char16_t |
U | Unicode32 字符 | char32_t |
L | 宽字符 | wchar_t |
u8 | UTF-8 | char |
后缀 | 最小匹配类型 | 后缀 | 类型 |
u 或 U | unsigned | f 或 F | float |
l 或 L | long | l 或 L | long double |
ll 或 LL | long long |
左部分的后缀适用于整型字面值,右部分适用于浮点型字面值。
变量(对象)定义
2.1变量定义
每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式。
变量定义的基本形式:类型说明符+变量名
当对象在创建时获得一个特定的值,说明这个对象被初始化,用于初始化的值可以是复杂的表达式
初始化的含义是创建变量时赋予其一个初始值,而赋值是将对象的当前值擦除,用新值来替代。
C++定义了多种初始化形式,这是初始化问题复杂性的体现。
int units_sold = 0;
int units_sold = {0};
int units_sold {0};
int units_sold (0);
用花括号来初始化变量是C++11新标准的一部分,这种初始化形式被称为列表初始化。
当用于内置类型的变量时,使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。 (内置类型就是算术类型和空类型)
如果定义变量时没有指定初始值,则变量被默认初始化,默认值由类型和变量的位置所决定。
如果内置类型的变量被默认初始化,值由定义的位置决定(上个变量在该位置遗留的数据)。
定义在函数体之外的变量被初始化为0。
2.2 变量声明和定义的关系
C++支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
声明使得该变量名被程序所知,而定义是为该变量创建相关的实体(数据/操作)。
变量声明规定了变量的类型和名字,定义还为变量申请存储空间。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,并且不要初始化变量。
extern是声明在其他源文件中定义的非static全局变量。
extern语句如果包含初始值就不再是声明,而变成定义了。
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将发生错误。
变量只能被定义一次,但是可以被多次声明。当我们要在多个文件中使用同一个变量,必须将声明和定义分离,变量的定义只能出现在一个文件中,其他用到该变量的文件必须对其进行声明。
2.3 标识符
C++的标识符(变量的命名)由字母、数字和下划线组成,其中必须以字母或下划线开头,长度不限,对大小写字母敏感。
用户自定义的标识符中不能连续出现两个下划线,也不能以下划线+大写字母开头。
定义在函数体外的标识符不能以下划线开头。
2.4 名字的作用域
作用域是程序的一部分,C++中大多数作用域都以花括号分隔。
定义在函数体之外的变量可以在整个程序的范围内使用,该变量拥有全局作用域,定义在函数体内部的变量在函数结束之前都能访问它,该变量拥有块作用域。
作用域之间可以相互包含,被包含的作用域被称为内层作用域,另一个作用域被称为外层作用域。
外层作用域一旦声明了某个名字,它所包含的所有作用域都能访问该名字。
允许在内层作用域中重新定义外层作用域已有的名字。(不推荐该操作)
int reused = 42;
int main()
{
int reused = 0;
std::cout << reused << std::endl; //输出 0
std::cout << ::reused << std::endl; //输出 42
return 0;
}
当我们在块作用域中重新定义全局作用域已有的名字,但想访问全面作用域的变量时,可以使用作用域操作符(::)来访问该变量。(操作符左侧为空时,向全局作用域请求该变量)
复合类型
复合类型是指基于其他类型定义的类型,例如引用和指针。
3.1 引用
引用:为一个已经存在的对象所起的另一个名字,通过在标识符前 + & 的形式来定义引用类型。
int val = 1024;
int &reval = val; // reval 指向 val (val的另一个名字)
定义引用时,程序把引用和它的初始值绑定在一起,因此,引用必须被初始化。
引用只能绑定在对象上,而不能与某个字面值或表达式的计算结果绑定在一起。
对象与引用的类型要保持一致,当引用的值被改变时,对象值也随着改变。
一个对象可以被多个引用,一个引用只能绑定一个对象,引用本身不是一个对象,所以不能定义引用的引用。
3.2 指针
指针与引用类似,也实现了对其他对象的间接访问。
指针本身就是一个对象,允许对指针赋值和拷贝,指针指向的对象可以进行改变,指针无需在定义时初始化,与其他内置类型一样,在块作用域内定义的指针如果没有被初始化,将拥有一个不确定的值。
定义指针的类型的方式:将声明符写成 *d 的形式,在一条语句中定义多个指针变量,每个变量前必须有符号*。
int *p; // 指针的定义
int *p1, *p2; //每个变量前都有*
指针存放某个对象的地址,要想获得该地址,需要使用取地址符&。
int a = 42;
int *p = &a; //指针p指向变量a,p存放a的地址
指针只能指向对象,因为引用本身不是对象,没有实际地址,所以不能定义指向引用的指针。
指针的类型要和其所指向的对象保持一致。
使用解引用符*来访问指针所指向的对象,改变解引用的结果,实际上也是在改变对象的值。
int a =42;
int *p = &a;
std::cout << *p; // 输出 42
*p = 0; //通过改变*p来改变a
std::cout << *p; //输出 0
解引用操作只适用于那些确实指向了某个对象的有效指针。
空指针不指向任何对象,在C++中有多个生成空指针的方法。
1、直接使用字面值nullptr来初始化指针
2、使用预处理变量NULL 来给指针赋值 ,需在开头添加头文件cstdlib。
3、直接给指针赋值0
对于预处理器,预处理器是运行于编译过程之前的一段程序,将预处理变量替换为实际值。
只要指针拥有一个合法值,就能将它用在条件表达式中。
对于两个类型相同的指针,可以使用相等操作符(==)来比较(比较它们所指向的地址值)。
void*指针是一种特殊的指针类型,可以用于存放任意对象的地址。
在同一条定义语句中,基本数据类型只有一个,但是声明符的形式可以不同。
int i = 1024, *p = &i, &r = i;
//i是int型的变量,p是指向i的int型指针,r是与i绑定的int型引用
涉及指针或引用的声明,一般有两种写法:
1、把修饰符和变量标识符写在一起。 2、修饰符和类型名写在一起,每条语句只定义一个变量。
int *p1, *p2; //第一种
int* p1; //第二种
int* p2;
上述两种定义指针或引用的方法并没有对错之分,仅看个人习惯,关键是坚持一种写法。
指针是内存中的对象,也有自己的地址,因此允许把指针的地址存放在另一个指针当中。
通过*的个数可以区分指针的级别,**表示指向指针的指针,以此类推。
int a = 42;
int *p = &a; // p指向a
int **pp = &p; // pp指向p
std::cout << *p << **pp << std::endl; // 输出 42 42
//当我们想访问最原始的对象时,需要对pp做两次解引用
引用本身不是对象,不能定义指向引用的指针,但指针是对象,存在对指针的引用。
int i = 42;
int *p = &i;
int *&r = p; //对指针p的引用
当我们面对一条复杂的指针或引用的声明语句时,从右往左阅读有助于你弄清楚它的真实含义。
const限定符
关键字const能帮助我们定义一种变量,该变量不能被改变,因此const对象必须初始化,初始值可以是任意复杂的表达式。
int i = 42;
const int ci = i; //i的值拷贝给ci
默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
如果想在多个文件之间共享const对象,在变量的定义之前添加extern关键字。
extern const int bufise = 1; //定义
4.1 const的引用
把引用绑定在const对象上,称为对常量的引用,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &ri = ci; //引用及其对象都是常量
在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果类型与引用的类型相同即可。
int i = 42;
const int &ri = i; // 允许将const int& 绑定到非常量i
对常量引用仅对引用可参与的操作做出限制,对于引用的对象本身是否为常量并不作限定。
int i = 1024; //普通int变量
int &ri = i; //引用绑定i
const int &r2 = i; //r2 绑定i,但是不允许通过r2去改变i
ri = 1; //可以通过其他途径去改变非常量i
4.2 指针和const
指向常量的指针不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。
const int i = 3;
const int *pi = &i;
默认情况下,指针的类型与所指对象的类型要保持一致。但是允许指向常量的指针指向一个非常量对象。
无论是指向常量的指针或引用,不过是指针或引用自认为自己所指向的常量罢了,自觉地不去改变所指对象的值。
指针是对象,允许把指针本身定为常量,名为常量指针,常量指针必须初始化。
常量指针存放初始值的地址将不能被改变,但是对象值可以被改变。
int numb = 0;
int *const cur = &numb; //常量指针cur
const int i = 10;
const int *const p = &i; //指向常量对象的常量指针p
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。
4.3 顶层const
在C++中,我们使用顶层const来表示指针本身是个常量,用底层const表示指针所指的对象是一个常量。
顶层const可以表示任意的对象是常量,这一点对任意数据类型都适用。底层const则与指针和引用等复合类型的基本类型部分有关。指针类型既可以是顶层const,也可以是底层const。
用于声明引用的const都是底层const,对常量取地址是底层const。
当执行对象的拷贝操作时,顶层const的部分基本没什么影响,而拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换,否则存在底层const对象被修改的风险。(一般来说,非常量是可以转换成常量)
顶层const 和底层const 的区分方法大概就是这个const是否是修饰这个变量本身的,如果是修饰变量本身的话,那么这个const就是顶层const,反之则是底层const。
4.4 constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
const int arr = 20; //arr是常量表达式
const int sz = get_size(); //sz不是常量表达式,因为它的具体值直到运行时才获取到。
C++允许将变量声明为constexpr类型以便编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int m = 40; // 40是常量表达式
constexpr int z = size(); // 只有当size是一个constexpr函数时,语句才是正确的
一般来说,如果你认为变量时一个常量表达式,那就将其声明为constexpr类型。
到目前为止所接触过的数据类型中,算术类型、引用和指针都属于字面值类型。
指针和引用都能定义成constexpr,一个constexpr指针的初始值必须是nullptr或者0,或是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量(当重新进入函数时,机器会重新给函数中的变量分配空间),相反的,定义于函数体之外的对象其地址固定不变,能用来初始化constexpr指针。
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关
const int *p = nullptr; //指向整型常量的指针
constexpr int *p1 = nullptr; //指向整数的常量指针,constexpr把它所定义的对象置为了顶层const
constexpr指针既可以指向常量也可以指向一个非常量。
处理类型
5.1 类型别名
类型别名是一个名字,是某种类型的同义词,使类型名字变得简单明了,易于理解和使用。
1、使用关键字typedef(typedef + 类型 + *别名 表示别名代表是该类型的指针)
2、使用别名声明,关键字using
typedef double wages; //wages是double的同义词
typedef double *p; //p是double*的同义词
using wages = double; // wages是double的同义词
如果某个类型别名指代的是复合类型或是常量,那么const是用来修饰基本数据类型的。
typedef char *p;
const p cstr = 0; //cstr是指向char的常量指针
const p *ps; //ps是一个指针,它的对象是指向char的常量指针
5.2 auto类型说明符
auto类型说明符让编译器通过初始值来推算变量的类型,auto定义的变量必须有初始值。
使用auto也能在一条语句中声明多个变量,但是一条声明语句只能有一个基本数据类型。
auto item = val1 + val2;
auto i = 0, *p = &i;
当我们使用引用来当作初始值时,真正参与初始化的其实时引用对象的值,此时编译器以引用对象的类型作为auto的类型。
auto一般会忽略顶层const,同时保留底层const。
int A = 1024;
const int i = 0;
const int &ri = i;
auto b = ci; //b是一个整数(ci的顶层const特性被忽略了)
auto d = &A; //d是一个整型指针(取i的地址)
auto e = &ci; //e是指向整数常量的指针(对常量对象取地址是一种底层const)
在auto前添加cosnt关键字,那么推断出来的auto类型是一个顶层const。
5.3 decltype类型指示符
decltype类型说明符的作用是选择并返回操作数的数据类型,适用于希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量的情况。
decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)。
decltype(f()) sum = x; //sum的类型是函数f的返回类型
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //x的类型是const int
decltype(cj) y = x; //y的类型是const int&,y绑定在变量x
引用从来都作为其所指对象的同义词出现,只有用在decltype处才被当作一个类型。
如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。
int i = 10, *p = &i, &r = i;
decltype(r+0)b; //加法的结果是int,因此b是一个int类型的
//如果想让结果类型是r所指的类型,可以把r作为表达式中的一部分
如果表达式的内容是解引用操作,则decltype将得到引用类型(推断出是数据的一个别名)。
decltype和auto的另一处重要区别是,如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型,如果给变量加上一层或多层括号,编译器会把它当成是一个表达式。 decltype的表达式如果是加上括号的变量,结果将是引用。
int i = 0;
decltype(i) c; //c是一个int
decltype((i)) d; //变量加上括号,结果是引用,d是int&
自定义数据结构
6.1 定义与使用类型
我们的类以关键字struct开始后面紧跟着类名和类体,类体以花括号包围成一个新的作用域。
类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。
struct Sales_data{
//定义类的变量之类的
}accum,trans,*sales; //类体后面可以紧跟变量名来表示对该类型对象的定义
类体内定义类的成员,修改一个对象的数据成员,不影响类体中的其他对象。
C++新标准规定,我们可以为数据成员提供一个类内初始值,没有初始值的成员将被默认初始化。
C++还提供另外一个关键字class来定义自己的数据结构。
6.2 编写自己的头文件
类一般都不定义在函数体内,为确保各个文件中类的定义保持一致,类通常被定义在头文件里,而且类所在头文件的名称应与类的名称一致。
头文件用来存放类的定义和一些const类型的定义,头文件也经常用到其他头文件的功能。
确保头文件多次包含仍能安全工作的常用技术是预处理器,当预处理器看到预处理变量时,会用指定内容替代这些变量或文件。
C++程序还用到一项与处理功能是头文件保护符,预处理变量有两种状态:已定义和未定义。
#define指令把一个名字设定为预处理变量
#ifdef当且仅当变量已定义时为真
#ifndef当且仅当变量未定义时为真
#endif指令用来终止上述指令为真执行后续操作
使用这些功能能有效地防止变量重复定义,一般把预处理变量的名字全部大写。