本章讲解C++内置的数据类型(如:字符、整型、浮点数等)和自定义数据类型的机制。下一章讲解C++标准库里面定义的更加复杂的数据类型,比如可变长字符串和向量等。
C++内置的基本类型包括:算术类型和空类型。算术类型值:字符、整型数、布尔值和浮点数;空类型对对应具体的值,仅用void表示用在一些特俗场合,比如函数不返回任何值时,就使用void作为返回类型。
算术类型分类整型和浮点型。
各算术类型在不同机器上表示的数字范围不一样,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位有效数字 |
其中:char的大小为一个字节,用于存放英语体系里面的任意字符;布尔类型的取值是真(true)或假(false);
内置类型的机器实现:
计算机按照二进制序列连续存储数据,每个bit非0即1,例如:
00001011110110101101011100001111......
只有将连续的一段bit规定为一个单位,二进制数据才有意义。C++规定,一个字节至少要能容纳机器中基本字符的所有字符。所以,一个字节有8位bit组成,一个字由32或64位bit组成。
由此,计算机中每8个bit使用1个地址,如下所示:
地址 数据 736424 1 0 1 0 0 1 1 0 736425 1 0 0 1 0 0 0 1 736426 1 1 1 0 1 1 1 0 736427 0 1 1 0 0 0 0 1 数据类型决定了某个具体数据所占的比特数以及这些比特位上数字的含义。
浮点型在C++中,被指定了最小有效位数,但是大多数编译器都实现了更高的精度。
除布尔型和扩展的字符型以外,其他整型可以划分为带符号(signed)和无符号(unsigned)的两种类型。带符号类型可以表示正数、负数或0;无符号类型仅能表示正数或0,写法如下:
unsigned int、unsigned long、unsigned char。
如果int、short、long和long long没有表示是否带符号,则默认是带有符号的,可以表示负数。
注意:浮点型是不能用unsigned和signed修饰的。
C++在类型的规定上有如此多的类型和规定,就是为了尽可能接近硬件,满足各种硬件的特性,所以显得有些繁杂。
注意:
切勿混用带符号类型和无符号类型,否则发生错误,运算结果无意义。
数据类型的的定义,决定了能包含的数据范围和运算。但是当代码中值与数据类型不匹配时,C++会进行自动数据类型转换。
- bool b = 42 // b为真
- int i = b; // i的值为1
- i = 3.14; // i的值为3
- double pi = i; // pi的值为3.0
把非布尔值赋值给布尔类型,0表示false,非0表示true;
把布尔值赋值给非布尔类型,false表示0,true表示1;
浮点数赋值给整型,仅保留整数部分;
整数赋值给浮点数,小数部分为0。如果整数过大,超过浮点类型容量,真数据失真;
每种数据类型的值,可以在程序中直接写出,被称作字面常量,比如:42。
每个字面常量都对应一种数据类型,其形式和值决定了它的数据类型。
数字可以是十进制、八进制、十六进制,为了区分这几种字面常量的不同,八进制和十六进制需要加前缀符号0和0X、0x,如下:
20 十进制 024 八进制 0x24十六进制 0X24十六进制
数字的字面常量,C++会以数字的大小,找到最小限度能装下该字面常量的数据类型与之匹配。整型经常是int型,但int装不下时,可能是long型;浮点型经常默认是double型。
字符字面值由单引号括起来,且只能写一个字符,比如'C';
字符串字面值由双引号括起来,里面可以写很多字符,本质上是由每一个字符所组成的数组,并以空字符('\0')表示结尾。所以,即使字符串里面只有一个字符,比如“C”,依然长度是两个字符,字符'C'和空字符。
两个字符串字面值写在一起,哪怕中间有空白字符(空格符、缩进符、换行符),也被C++认为是一个字符串,所以当书写较长的字符串时,一行不合适,可以分为两个字符串放在两行。
在字符串中,有两类字符不能直接使用,必须使用转义字符进行转义后才能使用。这两类字符是:
1.不可打印的字符,如退格、换行、空格或其它控制字符,因为没有可视化的符号;
2.在C++中有特殊含义的字符,如单引号、双引号、反斜杠、问号。
C++中转义字符如下:
换行符 \n | 问号 \? |
纵向制表符 \v | 进纸符 \f |
反斜杠 \\ | 报警符 \a |
回车 \r | 双引号 \" |
横向制表符 \t | 单引号 \' |
退格符 \b |
还有一种泛化的转义字符:格式1为:"\1到3个8进制数字";格式2为:“\x1到多个十六进制数字”,比如:
\7 响铃 \12 换行符 \40 空格 \0 空字符 \115 字符M \x4d 字符M
通过给字面值加上指定前缀和后缀,可以强制规定字面值的数据类型;
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode 16位字符 | chart16_t |
U | Unicode 32位字符 | chart32_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 |
布尔类型的字面值是:true和false;
指针字面值是:nullptr。
变量是一个有名称的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定了变量所占内存空间的大小和布局方式、能存储的值的范围,以及变量能够参与的运算规则。
变量定义的基本格式是:
数据类型说明符 变量名1, 变量名2, 变量名3 ...... ;
int a = 0, b, c=0; // a 和 c初始化了,b仅仅只是定义了
当变量获取第一个值的时候,称为初始化。初始化的值可以是任意形式:字面值常量、表达式结果、函数返回值等。
一条语句中初始化多个变量,前面的变量可以马上为后面的变量初始化。
double a = 0.1, b = a; // a 和 b的值都是0.1
注意:
初始化不是赋值,初始化的含义是创建变量时规定一个最初的值,而赋值是把变量当前的值擦除,用一个新的值代替。二者在内存中的操作动作不一样。
在C++ 11中引入,用花括号或括号初始化变量,如下都是正确的:
- // 以下变量值都是0
- int a = 0;
- int b = {0};
- int c{0};
- int d(0);
其中,花括号的形式逐渐流行,无论初始化还是赋值,都可以使用花括号。
如果定义变量时没有初始化,则变量被默认初始化,给赋予默认值。如果在函数体的变量没有初始化,则默认为0;如果在函数体内,如果变量没有初始化且没有赋值,则该变量值不可控,所以函数体内的变量一定要初始化或赋值。
C++可以把代码写在多个文件上,执行之前进行分别编译。如果要使用一个不在本文件中定义的变量,使用前需要声明,声明格式如下:
- // 对外部变量的声明
- extern 变量数据类型 变量名
变量只能被定义一次,但可以被声明多次,因为可以在不同文件中被使用。
变量声明、定义、初始化的区别:
- 声明:不分配存储空间,仅仅告诉该文件,这个变量使用了外部文件的变量;
- 定义:定义变量则分配了存储空间,但不一定有初值,即存储空间里不一定有具体的数据,但大多数情况,系统会进行默认值初始化。
- 初始化:不仅仅分配了存储空间,还给了一个具体的值。
- // 声明要使用外部文件中的一个变量i;
- extern int i;
-
- // 声明且定义了一个整型变量,并且,如果在函数体外则被默认初始化为0;
- int j;
-
- // 声明和定义一个变量c,并显式初始化了一个值'A';
- char c = 'A';
-
- // 如果extern被显式初始化了一个值,则失去声明外部变量的作用,变得无意义
- extern double s = 3.45533;
C++标识符的定义规则:由数字、字母或下划线组成,其中必须以字母或下划线开头。
标识符没有规定长度,但要区分大小写。
C++系统保留了一些标识符,我们在代码中不能使用这些标识符:
alignas | continue | friend | register | true |
alignof | decltype | goto | reinterpret_cast | try |
asm | default | if | return | typedef |
bool | delete | inline | short | typeid |
break | do | int | signed | union |
case | double | long | sizeof | unsigned |
catch | dynamic_cast | mutable | static | using |
char | else | namespace | static_assert | virtrual |
char16_t | enum | new | static_cast | void |
char32_t | explicit | noexcept | struct | volatile |
class | export | nullptr | switch | wchar_t |
const | extern | operator | template | while |
constexpr | false | private | this | and |
const_cast | float | protected | thread_local | and_eq |
auto | for | public | throw | bitand |
bitor | compl | not | not_eq | or |
or_eq | xor_eq | xor |
在C++中所有标识符定义后,它都有自己的作用范围,如果出了它能表示的作用范围,该标识符是另外一个含义,这个范围就是作用域。
C++中大多数的作用域都使用了花括号分隔。
- #include
-
- using namespace std;
-
- int sum = 1; // 全局作用域;
-
- int main(){
- int A = 0; // 块级作用域;
-
- for(int i = 0; i < 4; i++){
- int B = 1; // 作用域嵌套
- }
-
- int sum = 99; // 覆盖全局变量
- cout << ::sum << endl; // 使用全局变量
- }
注意:
如果函数要使用全局变量,则函数体内最好不要再定义一个同名的局部变量,以防混淆。
复合类型是指基于其他类型定义的类型。本章将先介绍:引用和指针。
引用(reference)就是为对象另外起一个名字,引用(refer to)对象,具体实例如下:
- int iVal = 322; // 初始化变量
- int &refVal = iVal; // refVal指向iVal(是iVal的另一个名字)
- int &refVal2; // 错误:引用必须初始化
引用将和它的初始值对象一直绑定,无法重新绑定,所以必须初始化。
- int iVal = 3233;
- int &refVal = iVal;
- refVal = 2;
- int i = refVal;
- int &refVal3 = refVal;
注意:
引用只能绑定到对象上,所以字面值或表达式的计算结果是不能被引用绑定。
引用本身不是对象。
指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用的区别:1.指针本身是对象,生命周期内可以先后指向不同的对象;2.指针可以不用初始化,但注意,不初始默认一个不确定的值。如下,指针的使用实例:
- int* ip1, * ip2; // ip1和ip2都是指向int型对象的指针;
- double dp, * dp2; // dp2是指向double型对象的指针,dp是double型对象
- int ival = 322;
- int* p = &ival; // 指针p存放变量ival的地址,或者说p是指向变量ival的指针
注意:引用不是对象,没有实际地址,所以不能定义指向引用的指针。
指针的类型必须与它指向的对象类型匹配,用&符号取对象的地址给指针赋值。
- double dval; //正确
- double* pd = &dval; //正确
- double* pd2 = pd; //正确
-
- int* pi = pd; //错误
- pi = &dval; //错误
使用*操作符对指针进行解指向,获取指针指向的对象:
- int ival = 45;
- int *p = &ival;
- cout << *p;
-
- *p = 0;
- cout << *p;
注意区分*和&操作符的含义:
int i = 334; int j = 22; int &r = i; int *p; p = &i; *p = j; int &r2 = *p;
空指针不指向任何对象,以下是生成空指针的三种方法:
- int *p1 = nullptr;
- int *p2 = 0;
- int *p3 = NULL; // 必须引用cstdlib头文件,其中定义了NULL预处理变量为0;
-
- // 不能将值为0的int变量作为空指针
- int zero = 0;
- int *p4 = zero; // 错误
注意:引用初始化后不能再赋值;而指针可以赋值,即把另一个新地址给指针,让指针指向另一个新对象。
一旦指针指向了一个确定的对象,可以用*操作符来访问这个对象。
- int ival = 334;
- int *p = &ival;
- cout << *p;
符号&和*被使用在引用和指针中,不同位置有不同作用,注意区分。
- int i = 42;
- int ii = 52;
- int *pi = 0;
- int *pi2 = &i;
- int *pi3;
-
- pi3 = pi2;
- pi2 = 0;
- pi = ⅈ
- *pi = 0;
-
指针在条件表达式里面的运用:1、如果是空指针,则条件取false;如果非空的指针,则条件取true; 2、条件表达式中可以使用==和!=符号进行两个指针的比较,如果两个指针指向同一个对象,则返回true,否则返回true;
void*指针是一个特殊的指针,可以指向任意对象。由于不知道对象的类型,所以不能使用*符号访问指向的对象。void*指针的作用有限:用它和其它指针比较、作为函数的输入或输出、或赋值给另一个void*指针。
- double obj = 3.14, *pd = &obj;
- void* pv = &obj;
- pv = pd;
指针的两种声明格式:
- int *p1 = 45;
- int* p2 = 56;
指针变量是内存中的一个对象,也是有地址的,所以指针变量的地址可以赋值给指针变量,**表示指向指针的指针,***表示指向指针的指针的指针:
- int ival = 3223;
- int *pi = &ival;
- int **ppi = π
- int *** pppi = &ppi;
引用本身不是对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
- int i = 53;
- int *p;
- int *&r = p; // r是一个指针的引用
-
- r = &i;
- *r = 0;
const修饰的对象,必须初始化,在此之后其值不能被改变。
const int bufsize = 512;
const对象仅在所定义的文件范围内有效,不加任何修饰是不能被其它文件引用的。
引用其它文件的const对象:在定义const对象的文件中必须使用extern修饰符来定义,然后在头文件里面使用extern声明,这样在其它文件里引用这个头文件就可以使用这个const对象了。
- // file_1.cc中定义并初始化了一个常量,extern修饰符使该常量能被其他文件访问;
- extern const int bufsize = fcn();
-
- // file_1.h头文件
- extern const int bufsize;
引用可以绑定到常量对象上,绑定后该引用和常量一样,不能被修改。
- const int ci = 1024;
- const int &r = ci;
- r = 42; // 错误,常量引用绑定的值不能修改;
- int &r2 = r; // 错误,普通引用不能绑定到常量引用指向的常量。
const引用可以绑定对象、字面值、表达式都可以:
- int i = 42;
- const int &r1 = i;
- const int &r2 = 42;
- const int &r3 = r1 * 2;
- int &r4 = r1 * 2;
const引用绑定变量,而变量值可以修改,其实是编译器复制这个变量为临时变量,而const引用绑定的是这个临时变量:
- int val = 34;
- const int& ref = val;
- cout << val + 1 << endl << ref;
底层const:要存放常量对象的地址,只能使用指向常量的指针:
- const double pi = 3.14; // pi是常量,其值不能改变
- const double *cptr = π // 正确
- const double pi = 3.14; // pi是常量,其值不能改变
- double *ptr = π // 错误,ptr是普通指针,不能指向常量
- const double *cptr = π // 正确
- *cptr = 42; // 错误,常量指针指向的值不能改变
const指针如果指向变量,则指针保持不变的是指向这个变量的地址,而变量的值可以通过其它方式修改:
- double dval = 3.14;
- double* ptr = &dval;
- dval = 3.1415926;
-
- cout << *ptr;
顶层const:一旦初始化地址,地址不能修改,这是常量指针,:
- double val = 6.3;
- double *const cptr = &val ;
需要区分底层const(指向常量的指针)和顶层const(常量指针):
- double dval = 3.14;
- double dval2 = 9.2;
-
- const double* ptr = &dval;
- double* ptr1 = &dval;
- double* const ptr3 = &dval;
- const double* const ptr4 = &dval;
-
- ptr = &dval2; // 错误:ptr3是不能修改指针里地址的const对象;
- ptr3 = &dval2;
- ptr4 = &dval2; // 错误:ptr4具备ptr3和ptr的全部特性;
-
- dval = 3.14159;
-
- *ptr = 99.0; // 错误:ptr是不能用*修改指向对象值的const对象;
- *ptr1 = 91.0;
- *ptr4 = 56; // 错误:ptr4具备ptr3和ptr的全部特性;
- cout << *ptr;
常量表达式是指值不会改变并且在编译的过程中就能得到计算结果的表达式:
- const int max_files = 20; // 是常量表达式
- const int limit = max_files + 1; // 是常量表达式
- int staff_size = 27; // 不是常量表达式,是字面值,staff_size是变量
-
- // 不是常量表达式,get_size()是函数调用,只有运行时才能知道值
- const int sz = get_size();
类似于上面的get_size不是常量表达式,所以C++11标准规定了constexpr关键字,以供编译器判断哪些是我们设定的常量表达式:
- constexpr int mf = 20;
- constexpr int limit = mf + 1;
- constexpr int sz = size(); // 注意:此处size函数的定义中使用constexpr关键字。
常量表达式的值需要在编译时得到计算,因此对声明constexpr时用到的类型,常使用字面值。目前,算术类型、引用和指针都属于字面值类型。
将指针定义成constexpr,其值只能是nullptr或另一个constexpr类型的地址。
- constexpr int* q = nullptr;
- int j = 0;
- constexpr int i = 42;
- constexpr const int* p = &i;
- constexpr int* p1 = &j;
类型别名是一个名字,是其它某种类型的同义词。让复杂的类型名字变得简单、易于理解和使用。
- typedef double money; // wages是double的同义词
- typedef money base, *p; // base是double的同义词;p是double*的同义词;
在C++11标准中,新表示方法:
using money = double;
auto类型说明符,能让编译器去分析表达式所属的类型,让编译器通过初始值来推算变量的类型。因此,auto定义的变量必须有初始值。
- int a = 12;
- int b = 22;
- auto c = a + b;
-
- auto i = 0, *p = &i; // i是整数、p是指向整数的指针
- auto sz = 0, pi = 3.14; // 错误,sz和pi的类型不一致
decltype可以从表达式的类型推断出要定义的变量类型,而不使用表达式的初始值。因此,decltype必须初始化。
- decltype(main()) sum = 98; // 因为main函数的返回值是int型,所以sum的类型是int
-
- const int ci = 0, &cj = ci;
- decltype(ci) x = 0; // x的类型是const int
- decltype(cj) y = x; // y的类型是const int &, y是绑定到变量x上的引用
- decltype(cj) z; // 错误,没有初始化
通过struct定义自己的数据类型:struct开始,紧接着结构名和结构体(可以为空)。结构体由花括号包围形成一个新的作用域,结构内部定义的成员名唯一。结构的后面可以直接定义结构对象,所以末尾必须由分号结束。结构体内部的成员,最好初始化否则默认值未知。
- struct Student{
- std::string name = "";
- int age = 0;
- int classNum = 0;
- int grade = 0;
- bool gender = 0;
- } Zhangsan, Lisi;
结构对象的定义和使用:
- Zhangsan.name = "张三";
- Zhangsan.age = 10;
- Zhangsan.grade = 5;
- Zhangsan.gender = 1;
-
- Lisi.name = "李四";
- Lisi.age = 9;
- Lisi.grade = 4;
- Lisi.gender = 0;
-
- cout << Zhangsan.name << endl;
- cout << Lisi.name << endl;
- cout << Lisi.age << endl;
结构体一般都不会定义在函数体内。如果要在不同文件中使用同一个结构体,结构的定义就必须保持一致。
为了确保各个文件总结构的定义一致,结构通常被定义在头文件中,而且结构所在头文件的名字应与结构的名字一样。
头文件通常包含那些只能被定义一次的实体,如结构、const和constexpr变量等。头文件中也经常需要用到其他头文件的功能。头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。
预处理器:在编译之前执行的一段程序,可以部分改变我们编写的代码。比如,之前使用的#include预处理功能,当编译器看到#include标记时就会用指定的头文件的内容代替#include。
头文件保护,定义预处理变量,其状态只能是:已经定义和没有定义。#define指令把一个名字设定为预处理变量;#ifdef当预处理变量已定义为真;#ifndef当预处理变量没有定义时为真。从#ifdef或#ifndef开始,一直运行到#endif结束。
预处理变量无视C++语言中关于作用域的规则,所以通常预处理变量全部大写。
- // Student.h
-
- #ifndef STUDENT_H
- #define STUDENT_H
- #include
-
- struct Student {
- std::string name;
- int age;
- int classNum;
- int grade;
- bool gender;
- };
-
- #endif
-
程序中,引用系统头文件使用尖括号,引用自定义头文件使用双引号:
- #include
- #include "Student.h"
-
- using namespace std;
-
- int main()
- {
-
- Student Zhangsan, Lisi;
-
- Zhangsan.name = "张三";
- Zhangsan.age = 10;
- Zhangsan.grade = 5;
- Zhangsan.gender = 1;
-
- Lisi.name = "李四";
- Lisi.age = 9;
- Lisi.grade = 4;
- Lisi.gender = 0;
-
-
- cout << Zhangsan.name << endl;
- cout << Lisi.name << endl;
- cout << Lisi.age << endl;
-
- return 0;
- }