2.数据类型
引言:在第二章数据类型中,只对数据类型进行简单介绍,剩下的会在下一章中对其进行补充。
2.1基本整型
计算机内存的基本单位是位(bit),由0和1组成。若是8位的内存,则可以设置256种组合,可以用来表示数值,其范围是0~255。
字节(byte)通常指的是8位的内存单元。1KB = 1024byte,1MB = 1024KB。C++中定义一个字节至少能容纳一个基本字符集中的字符。在美国,基本字符集通常是ASCⅡ和EBCDIC,它们都可以用8位来容纳。而国际编程需要使用更大的字符集,如Unicode,因此需要使用16位或32位的字节。为了统一,后面默认一个字节是8位。
根据编译器的不同,类型的宽度也不同。在C++中提供了一个标准:short ≤ int ≤ long ≤ long long;short ≥ 16bit,long ≥ 32bit,long long ≥ 64bit。在64位的windows系统中,查看C:\Program Files (x86)\Microsoft Visual Studio 2017\2017\Community\VC\Tools\MSVC\14.14.26428\include目录下limits.h文件如下:
图2-1
为后面的代码统一类型宽度,以图2-1为标准。short为16位,int为32位,long为32位,long long为64位。
基本整型的类型都明白了,该怎么使用呢?有3种方式:
1.变量声明和赋值:
int a;
a = 10;
2.变量初始化(等价于声明和赋值)
int a = 10;
3.C++11初始化方法:
int a = {10}; //初始化10
int a{10}; //初始化10
int a = {}; //初始化0
int a{}; //初始化0
前面所说的4种基本整型都是有符号整型,而它们都有各自的无符号整型(非负)。unsigned short、unsigned int(或unsigned)、unsigned long、unsigned long long是无符号整型。如short范围是-32768~32767,unsigned short的范围是0~65535。
整型的字面值可以表示为八进制、十进制、十六进制。八进制是以0开头的整数,十六进制是以0x开头的整数,若表示20,则:
20 //十进制
024 //八进制
0x14 //十六进制
2.2浮点类型
C++有3种浮点类型:float、double、long double。float的有效位数大于等于6,数值宽度大于等于32位;double的有效位数大于等于10,数值宽度大于等于48位;long double的有效位数大于等于10,数值宽度大于等于double的位数。通常,float有7个有效位数,32位宽度;double有16个有效位数,64位宽度;long double有96或128位宽度,有效位数不定。
对于浮点常量通常默认为double型,若想表示为float型,则使用f、F为后缀;若想表示为long double型,则使用l、L为后缀。
1.234 // double
1.234f // float
1.234L //long double
浮点类型的使用和基本整型一样,不在多述。
2.3布尔类型
bool类型的取值是真(true)和假(false)。bool类型的使用和基本整型、浮点类型相同,共有3种方式,不在多述。
当把非bool类型的数值赋给bool类型时,数值为0则结果为false,数值非0则结果为true;当bool类型的数值赋给非bool类型时,false则结果是0,true则结果是1。
bool a = 10.1; //a = 1,即a为真
int b = a; //b = 1
2.4字符
基本的字符类型是char,根据前面字节的定义,我们知道一个char的大小和一个机器字节一样,因此char的位数为8 bite。
与int不同,char在默认情况下,既不是没有符号,也不是有符号。因此,字符类型被分为3种:char、signed char、unsigned char。如果将char用作数值类型,则signed char表示范围是-128~127,unsigned char表示范围是0~255。
除了3种基本字符类型(用于基本字符集),还有其他的字符类型用于扩展字符集,如wchar_t、char16_t、char32_t。wchar_t用于确保可以存放机器最大扩展字符集中的任意一个字符;char16_t、char32_t是C++11添加的,专门为Unicode字符集服务。
注:若程序处理的字符集无法用8位的字节来表示,则C++处理方式有2种:1.若基本字符集是大型字符集,编译器将char宽度定义为16位或更长的位数(字节的位数也改变为16或其他)。2.若同时支持一个小型的基本字符集和大型的扩展字符集,8位用char表示,16位用wchar_t表示。
char的3种使用方式:
1.声明+赋值
char cr;
cr = ’A’;
cout<<”cr = ”< 2.初始化 char cr = ’A’; 3.char转换为int char cr = ’A’; int A = cr; // A = 65 wchar_t类型可以通过加前缀L来表示宽字符常量或宽字符串常量;其输入输出使用wcout、wcin。2种使用方式: 1.声明+赋值 wchar_t wct; wct = L’A’; wcout<<”wct = ”< 2.初始化 wchar_t wct = L’A’; char16_t、char32_t都是无符号类型,宽度分别是16位和32位。char16_t前缀加u表示char16_t型字符常量或字符串常量;char32_t前缀加U表示char32_t型字符常量或字符串常量。 2.5枚举 枚举的作用是定义新的类型,用新类型来创建符号常量。共2种使用方式: 1.默认数值 enum colour {red, orange, yellow, green, blue}; //这些符号不要用双引号括起来 colour cor; cor = orange; cout << "cor = " << cor << endl; //打印 cor = 1 代码解释: 首先定义了枚举类型colour,此类型中的符号常量分别是red...blue,分别对应数值0~4。再定义colour枚举的变量cor,其取值范围只能是red...blue。 枚举类型可以向int型转换: int tinct = yellow; //tinct = 2 可以省略枚举类型,直接使用符号常量: enum {red, orange, yellow, green, blue}; int tinct = 2; if (tinct = yellow) { cout << "tinct = " << yellow << endl; //打印 tinct = 2 } 2.非默认数值 enum colour {red, orange, yellow = 100, green, blue}; 代码解释: 可以部分赋值,也可以全部赋值。上面采用了部分赋值,即red = 0,orange = 1,yellow = 100,green = 101,blue = 102。 其它使用都和默认数值一样,不再多述。 2.6同类数据的存储 2.6.1数组 若需要存储多个同类型的数据,则可以使用数组。数组[]内的值表示数组元素的总数,但是数组元素的下标是从0开始的。数组的3种使用方式: 1.声明+赋值 int abc[5]; abc[0] = 10; abc[3] = 11; abc[4] = 10; 在声明(类型为int数组)后进行赋值,不可以一次性赋多个值,只能一个一个赋值,并且未赋值的数组元素会是一个随机值(不是0)。数组声明必须给定数组元素的总数。因为这是静态数组,属于静态联编,在编译时就已经确定数组大小。 注:数组不能整体赋值,数组声明或初始化时必须指定元素总数。 2.初始化 int abcd[5] = {12,13,14}; 未初始化的元素,编译器会自动设置为0.如abc[3]、abc[4]为0。 int abcd[] = {12,13,14}; 若数组在初始化是未给定元素总数,则C++编译器自动计算数值,即abc元素总数为3,相当于int abcd[3] = {12,13,14}。 3.C++11初始化 int abcde[5] {12,13,14}; //去掉 = int abcde[5] = {}; //初始化为0 int abcde[5] {}; //初始化为0 2.6.2模板类vector 前面创建的数组都属于静态数组,若要创建动态数组,该怎么办?在C++的头文件vector中,通过模板类vector来实现动态数组,模板类vector包含在名称空间std内。模板类vector有2种使用方式: 1.模板类vector对象的元素长度未知 #include #include int main(void) { using namespace std; vector<int> vi; vi.push_back(1); cout << "vi[0] = " << vi[0] << endl; //打印 1 } vi是类vector<int>的对象,它可以当成一个int型动态数组。vi的初始长度设置为0,若要调整vi数组长度,如进行插入元素或删除元素等操作,需要调用模板类vector中的成员函数。 2.模板类vector对象的元素长度已知 #include #include int main(void) { using namespace std; vector<int> vi(1); vi[0] = 1; vi.push_back(2); cout << "vi[0] = " << vi[0] << endl; // 打印vi[0] = 1 cout <<" vi[1] = " << vi[1] << endl; // 打印vi[1] = 2 } vi是类vector<int>的对象,在声明时确定只有1个int元素,因此可以使用vi[0] = 1;但若是要增加vi长度,则只能使用vi的成员函数push_back()。 4种方式来初始化模板类vector对象(长度已知) vector<int> via(8); //初始化via,其可以包含8个int元素 vector<int> vib(8,0); //初始化vib,其包含8个0 vector<int> vic(vib); //初始化vic,其元素和vib相同,深拷贝 vector<int> vid(vic.begin(),vic.begin()+4);//初始化vid,其包含vic的第1-4元素,深拷贝 2.6.3模板类array 若需要存放一组长度固定的元素,可以采用数组。但是数组的使用并不安全,数组没有边界检查,你在使用的过程中可能会造成数组越界(int a[10];a[20] = 0;不会报错)。因此C++11新增了模板类array(头文件array),其和vector一样包含在名称空间std中,但模板类array和模板类vector不同,其不能添加或删除元素来改变长度。 模板类array和数组都是存放固定长度的同类数据,其不同之处: 1.模板类array可以一次赋多个值或者直接把另一个模板类array(元素类型和长度必须相同)直接赋给它;数组一次只能赋一个值且不能整体数组赋值。 2.数组越界,运行时没有问题;模板类array越界,运行会中止。 #include #include int main(void) { using namespace std; int i[4] = { 1,2,3,4 }; array<int, 4> ai; //声明 ai = { 1,2,3,4 }; //赋值 //或者直接初始化array cout << "ai[0] = " << ai[0] << endl; ai[1] = 10; //修改元素 cout << "ai[1] = " << ai[1] << endl; // 打印ai[1] =10 cout << "ai[1] = " << ai.at(1) << endl; // 打印ai[1] =10 //cout << "i[5] = " << i[5] << endl; 可以运行 //cout << "ai[5] = " << ai[5]<< endl; 运行中止 //cout << "ai[5] = " << ai.at(5) << endl; 运行中止,抛出out of range异常 array<int, 4>::iterator aiir; for (aiir = ai.begin(); aiir != ai.end(); aiir++) //迭代器的使用 cout << *aiir << endl; return 0; } 2.7字符串 C++处理字符串有2种方式,1.来自C语言的C-风格字符串;2.基于string类库的字符串。 2.7.1 C-风格字符串 C-风格字符串以空字符结尾,即‘\0’。因此用数组表示固定长度字符串时,其元素总数要加一。C-风格字符串的使用方式有3种: 1.数组表示固定长度字符串 char cr[5] = {'c','h','a','r'};//不可以char cr[5];cr[5] = { 'c','h','a','r'}; char cr[5] = “char”; //不可以char cr[5]; cr[5] = “char”; 字符串数组不可以整体赋值,可以整体初始化;并且以这种方式表示字符串,其数组长度必须比元素总数多1。当数组内存中存放完字符串,还剩下的元素空间自动补空字符‘\0’。 #include int main(void) { using namespace std; const char cr[5] = {'c','h','a','r'}; //const表示只读 for (int i = 0; i < 5; i++) cout << cr[i] << endl; //或cout << cr<< endl; char str[8] = "string"; for (int i = 0; i <8; i++) cout << str[i] << endl; str[0] = 'c'; cout <<"str[0] = "<< str[0] << endl; return 0; } 注:若这样定义char cr[4] = {'c','h','a','r'},在字符串读取时,读完‘r’后会继续读,直到遇到‘\0’为止。 2指针表示固定长度字符串 const char * name = "zhangsan"; //初始化 或 const char * name; //声明 name = "zhangsan"; //赋值 这表示cr指向一个长度为5的char型数组,cr的值表示数组第一个元素的地址。使用带双引号的字符串来初始化(或声明+赋值)字符指针时,必须加const表示字符串只读。若不想用const,可以使用字符数组来初始化字符指针: char name[20] = "zhangsan"; char * cr; cr = name; cout << name << endl << cr << endl; 总结:当字符串只读时,可以使用双引号的方式来初始化字符指针;当字符串可写时,需要使用字符数组来初始化字符指针。 注:数组可以使用cin来输入字符串,而指针不能,指针需要初始化才能用cin(数组的声明和指针不同,数组在声明时就会开辟内存,而指针声明后会随机一个垃圾值,此时读写指针太危险,编译器不会通过): char cr[20]; cin >> cr; //可以 而: char *cr; cin >> cr;//不可以 3指针表示未知长度字符串 动态开辟内存,使用new和delete来分配释放内存。 #include #include int main(void) { using namespace std; int n; cout << "please input the number of your name:"; cin >> n; char * cr = new char[n + 1];//在堆空间中开辟可以存放n+1个char类型的内存(char数组) char name[20]; cout << "please input your name:"; cin >> name; strcpy_s(cr, n+1, name);//strcpy不安全,strcpy_s计算空字符 cout << "your name is "<< cr << endl; delete []cr; //释放char数组 return 0; } 2.7.2 string类 #include #include int main(void) { using namespace std; string str1 = "zhangsan"; string str2; const char * cr= "lisi"; str2 = cr; string str3; char ar[20] = "wanger"; str3 = ar; cout << str1 << endl << str2 << endl << str3 << endl; str1 = "zhangsan2"; str2 = "lisi2"; str3 = "wanger2"; cout << str1 << endl << str2 << endl << str3 << endl; cout << cr << endl ;//打印:lisi return 0; } 代码解释: 代码中使用3种方式来初始化string类的对象,直接使用双引号字符串初始化;使用字符指针初始化;使用字符数组初始化。 为什么cr的类型是const char *,赋值给str2后,str2可以修改?在C++中拷贝分为2种方式:浅拷贝和深拷贝。以拷贝字符串为例,浅拷贝就是将a字符串的首地址赋给b,b通过字符串首地址来对a进行读写,其实它们操作的是同一块内存的字符串。深拷贝就是b重新开辟一块内存来存放和a相同的字符串,它们操作的是两块不同内存的字符串。 在string类的成员函数中,有=运算符重载函数,其对传入的字符串进行处理,即使用new来开辟空间。 2.8指针 有数据就有数据存放的地址,在C/C++使用指针来访问数据的地址(一个地址可以存放二进制数据的位数,称为位宽,它需要根据芯片的数据线总数和其连接方式来确定,这里不讨论)。 2.8.1 指针和基本整型 int count =3; int * pt; //声明 pt = &count; //赋值 或 int count =3; int * pt = &count; //初始化 pt是int型指针,即类型为int *;其指向一个int数据count,即pt的数值是count的地址;*pt是count的值,即*pt = count。 2.8.2指针和数组 #include int main(void) { using namespace std; int ia[5] = { 1,2,3,4,5 }; int * ipt; ipt = ia; char ca[5] = "char"; char * cpt; cpt = ca; cout << cpt << endl; for (int i = 0; i < 5; i++) cout << cpt[i]; cout << endl; for (int i = 0; i < 5; i++) cout << *(cpt+i); cout << endl; cout << ipt << endl; cout << &ia[0] << endl; for (int i = 0; i < 5; i++) cout << ipt[i] << ' '; cout << endl; for (int i = 0; i < 5; i++) cout <<*(ipt+i)<< ' '; return 0; } 代码运行: 为什么打印cpt就可以把字符串打印出来,而打印ipt只打出了数组第一个元素的地址?在前面我们使用char数组和char*来初始化数组,而数组名本身就代表数组存放的地址(第一个元素的地址),因此可以得知字符串的本质类型是char *。当我们打印cpt时,cpt的数据类型是char *,所以打印的其实是以cpt为起点的字符串,直到遇到空字符为止。 用指针来表示数组元素,通常有2个方法,即ipt[i]和*(ipt+i)。 2.8.3指针的危险 1.空指针 当指针不指向任何一个地址,这样的指针称为空指针。若pt是一个指针: pt = 0; pt = ‘\0’; pt = NULL; 这些都可以表示pt是一个空指针。 在C++11中,用nullptr来表示空指针,这也是C++11的新标准。 在使用new/malloc时,若分配内存失败会返回一个空指针,若不进行判断指针,则在后面使用这个指针时会出现很大的问题。 解决方案:若返回值是指针,则if(p!=NULL)...。 2.指针声明 指针声明后,会随机给一个垃圾值,而不是空指针,此时读写指针非常危险,因此在定义指针时尽量初始化,哪怕是nullptr也好。 #include int main(void) { using namespace std; int * pt = nullptr; int count = 5; pt = &count; cout << *pt << endl; return 0; } 3. 内存泄漏 程序运行需要内存,内存是计算机有限且宝贵的资源。在C/C++中使用malloc/new来申请内存,通过动态分配得到堆空间上的内存,当我们使用完后需要及时手动释放(栈上的内存在使用完后,系统会自动收回)。若不释放,会造成内存占用率越来越高,严重会导致进程崩溃,这就是内存泄漏。 解决方案:malloc/free, new/delete成对使用。 4.野指针 野指针也称悬挂指针,野指针不是空指针。 在程序中会采用多指针指向同一块堆内存(堆空间由new/malloc开辟,而局部变量存放在栈空间,随函数结束会自动销毁),但这会带来问题: 其中一个指针delete后,其他指针无法判断是否有效。解决方案:谁申请,谁释放。 当指针释放后,需要把指针置为nullptr(空指针),因为指针释放只是释放那一块堆内存,而指针的值还是那一块内存的值。并且还需要把其他指向这块内存的指针置为nullptr。解决方案:维护一个指针计数,当多一个指针指向这块内存,计数加一;一个指针销毁或者不指向这块内存或置为nullptr,计数减一;当计数为0时,释放内存(智能指针)。 2.9引用 引用:为对象起另外一个名字,即别名。 #include int main(void) { using namespace std; int val = 20; int &ival = val; val++; cout << ival << endl; //打印21 ival++; cout << val << endl; //打印22 return 0; } 代码解释: 初始化val为20,类型为int引用的ival对val进行引用,因此ival就是val的别名,它们操作的是同一块内存。val变化,ival也变化;ival变化,val也变化。 注:引用必须初始化,不可以int &ival;ival = val。 2.10不同类型数据的存储 对于相同类型的数据,我们可以使用数组、模板类vector、模板类array来存储,而对于不同数据类型的数据改怎么样存储呢? 2.10.1结构体 通过定义一种新的数据类型,可以存储多种类型的数据,这种类型称为结构体,它是由用户来定义的。 #include #include //不能使用primary school,必须用_连接 enum grade { primary_school, middle_school, collge }; //在结构体student的定义中,每一项都要用“;”来分割,包括最后一项 struct student { //必须使用std::string,因为在结构体student定义之前未声明名称空间的使用 std::string name; int age; grade gra; }; //结构体定义后面必须有分号 int main(void) { using namespace std; /*在变量stu1(类型是student结构体)的初始化中,每一项用“,”隔开,不包括最后一项(同数组、枚举)*/ student stu1 = { "zhangsan", //称为变量stu1的name成员 10, primary_school }; cout << stu1.name << endl<<stu1.gra<< endl; //枚举值得打印只能打印出数字 switch (stu1.gra) { //采用switch来打印枚举值 case 0:printf("primary_school\n"); break; case 1:printf("middle_school\n"); break; case 2:printf("collge\n"); break; default:printf("none\n"); } return 0; } 代码解释: 要使用结构体,必须先定义结构体,在main函数的代码上面定义了结构体student,在这里新的数据类型是结构体student;在结构体student中定义的name、age、gra称为结构体student的成员;在main函数中的stu1是结构体student的变量。 除了整体对student的变量stu1初始化外,还可以分别来初始化stu1的成员: student stu1; stu1.age = 10; stu1.gra = primary_school; stu1.name = "zhangsan"; 至于变量的声明还可以在结构体定义的后面来声明: struct student { std::string name; int age; grade gra; }stu1,stu2; 在结构体定义后面直接初始化也可以: struct student { std::string name; int age; grade gra; }stu1 = { "zhangsan", 10, primary_school }; 2.10.2共用体 共用体类似于结构体,在定义中可以存储不同的数据类型,但其变量只能存储其中一种数据类型。 #include #include using namespace std; union student { int age; const char *name; //不可以用string类;用char数组,不可以整体赋值 }stu1; int main(void) { stu1.age = 10; cout << stu1.age << endl; stu1.name = "zhangsan"; cout << stu1.name << endl; return 0; } 共用体student的变量为stu1,其中定义了2个成员,分别是age、name。在stu1成员的赋值中,当赋值给age,则name是空的;赋值给name,则age是空的。也就是说age和name公用一段内存,其大小是最大成员的长度。 2.11类 面向对象编程有三大特性:封装性、继承性、多态性。为了实现这3大特性,C++对结构体进行改进,增加了一种新的数据类型:类。 同类数据的存储使用数组,不同类型数据的存储使用结构体。当我们把不同类型的数据和对数据的操作(函数)集成在一起,于是就出现了类。 #include #include using namespace std; class Student { public: const char* name; string curriculum; //课程 int grade; //成绩 public: void printinfo(void) { cout << "name = " << name << " curriculun = " << curriculum << " grade = " << grade << endl; } }; int main(void) { Student stu1; stu1 = { "zhangsan","English",80 }; stu1.printinfo(); return 0; } 类的定义和结构体一样,需要在尾部加上分号。同时,为了约定书写习惯,在后面的类定义中,将第一个字母大写,即Student。 在这里不再对类进行多介绍,在后面的章节中,会根据对象化编程的3大特性来对类进行分析。