CPP 4. 复合类型 2020-10-15 浏览量 720 暂无评论 # 第4章 复合类型 最深远的复合类型是类。 ## 4.1 数组 ### 4.1.1 声明方法 ```cpp typeName arrayName[arraySize]; short months[12]; //从0开始编号 ``` 注意: 1. 编译器不会检查使用的下标是否有效,当下标超出数组范围时,编译器并不会报错,但是程序运行后,会引发问题,也可能导致程序异常终止; 2. 存储多个同类型的值; 3. 在内存中是连续的。 ### 4.1.2 初始化规则 1. 只有在定义数组时才能使用初始化,不能先声明再直接对整个数组赋值,但是可以使用下标分别给数组中的元素赋值; ```cpp int card[4] = {3, 6, 8, 10}; // okay int hand[4]; // okay hand[4] = {5, 6, 7, 9}; // not allowed hand = cards; // not allowed hand[0] = 3; // okay ``` 2. 初始化数组时,提供的值可以少于数组的元素数目,剩余的会被编译器自动设置为0,这在将数组中所有元素都初始化为0时很有用; 3. 如果初始化数组时方括号内([])为空,C++编译器将会自动统计元素个数(在初始化数组时通常别这样做,但初始化字符串时这样很方便); 4. 初始化数组时,可省略等号(=)(C++11); 5. 初始化数组时,可不在大括号内包含任何东西,默认为0(C++11); 6. 初始化时禁止缩窄转换(C+11)。 **ps:** C++标准模板库提供了数组的替代品--模板类`vector`,C++11新增了模板类`array`,比内置复合类型数组更复杂、更灵活。 具体怎么灵活呢? ## 4.2 字符串 ### 4.2.1 字符串初始化 注意: 1. 字符串在内存中也是连续的,可以存储在char数组中; 2. C-风格字符串以空字符(null character)结尾,被写作\0,其ASCII码为0,用来标记字符串的结尾,不是空格; 3. 使用引号括起的字符串常量,隐式地包括结尾的空字符; 4. `''`和`""`的区别很大,一个是字符常量,一个是字符串常量,如`"S"`表示两个字符(字符S和\0)组成的字符串。 ```cpp char dog[8] = {'b', 'e', 'a', 'u', 'x', ' ', 'I', 'I'}; // not a string char cat[8] = {'f', 'a', 't', 't', 'e', 's', 's', 'a', '\0'}; // a string char fish[] = "Bubbles"; // a string char shirt_size = "S"; // illegal type mismatch ``` ### 4.2.2 字符串拼接 😄任何两个由空白(空格、制表符、换行符)分隔的字符串常量都将自动拼接成一个,第一个字符串的\0字符将被第二个字符串的第一个字符取代。 ### 4.2.3 在数组中使用字符串 **string.cpp** ```cpp #include #include // for strlen() function int main() { using namespace std; const int Size = 15; char p1[Size]; char p2[Size] = "却又吹得让它慌张\n"; cout << "你问风为什么托着候鸟飞翔, " << p2; cin >> p1; cout << "却也湿透她的衣裳\n"; cout << "你问我为什么还是不敢放下, " "明知听不到回答\n"; } ``` 注意: 1. `strlen()`函数返回存储在数组中的字符串的长度,只计算可见的字符,不包含空字符; 2. `sizeof()`运算符统计整个数组的长度。 ### 4.2.4 字符串的输入 **insre1.cpp** ```cpp #include int main() { using namespace std; const int ArSize = 20; char name[ArSize]; char dessert[ArSize]; cout << "Enter your name:\n"; cin >> name; cout << "Enter your favorite dessert:\n"; cin >> dessert; cout << "I have some delicious " << dessert; cout << " for you, " << name << ".\n"; return 0; } ``` >Enter your name: >**Jineng Dreeb** >Enter your favorite dessert: >I have some delicious Dreeb for you, Jineng. 在本例中,都没有对“Enter your favorite dessert:”作出反应,程序就已经显示出来了,为什么呢?这关系到`cin`如何确定已完成对字符串的输入。 由于不能通过键盘输入空字符,因此`cin`使用空白(空格、制表符、换行符)来确定字符串的结束位置,`cin`在获取字符数组时只读取一个单词,然后将该字符串放在数组中,并自动在结尾添加空字符,后面的单词留在输入队列中,等待下一次读取。 ### 4.2.5 每次读取一行字符串输入 C++常使用指针来处理字符串。 注意: 1. `cin.getline()`每次读取一行,读到换行符,随后丢弃换行符,保存时将换行符替换为空字符; 2. `cin.get()`读到换行符,将换行符留在队列中; 3. `cin.get()`用来检查错误更方便,通过读取下一个字符是不是换行符可以知道之前读的字符串是否超出了数组的长度; 4. *`cin.get()`* 在读取空行后将设置失效位(failbit),接下来的输入将会被阻断(**直接回车,程序直接退出了**),但可用`cin.clear()`来恢复输入; 5. 当输入的字符串比分配空间长时,都会把余下的字符留在输入队列中,而 *`cin.getline()`* 还会设置失效位,并关闭后面的输入(**直接回车,程序直接退出了**)。 以下语句等价: ```cpp cin.getline(name1, ArSize).getline(name2, ArSize); cin.get(name1, ArSize).get().getline(name2, ArSize).get(); // cin.get()读取下一个字符(即使是换行符) ``` ##### 4.2.6 混合输入字符串和数字 如下程序会出现问题,用户没有输入地址的机会。因为cin读取年份后,将回车键生成的换行符留在了输入队列,getline会把换行符当作空行赋给address数组,可以在cin后加get。 ```cpp int year; cin >> year; char address[80]; cin.getline(address, 80); ``` ## 4.3 String类 **特点:** 1. 自动调整长度; 2. 使字符串操作更容易。 ### 4.3.1 初始化 ```cpp string str1; cin >> str1; // str1开始长度为0,但将输入读取到str1中时,将自动调整str1长度 string str2 = "panther"; // C++11 char first[] = {"JHAN"}; char second[] {"JHAN"}; string third = {"JHAN"}; string fourth {"JHAN"}; ``` ### 4.3.2 赋值、拼接和附加 不能将一个数组赋给另一个数组,但是可以将一个string对象赋给另一个string对象。 ```cpp string str3; str3 = str1 + str2; // 拼接str1和str2给str3,cstring strcpy (这个没法自动调整长度,可用stmcpy接受第三个参数) str1 += str2; // 把str2加到str1后面,cstring strcat (这个没法自动调整长度,可用stmcat接受第三个参数) string str; getline(cin, str); ``` ##### 4.3.3 原始字符串(C++11) ```cpp cout << R"(jhan "jhan" "\n")" << '\n'; cout << R"+*("(jhan)", jhan)+*" << '\n'; ``` >jhan "jhan" "\n" >"(jhan)", jhan ## 4.4 结构简介 **特点:** 1. 存储多种类型的数据; 2. 使用运算符(.)来访问各个成员; 3. 外部声明(位于函数外面)和内部声明(位于main()函数内部)的作用域,若内部声明和外部声明名称重复,则在函数内部以内部声明为准; 4. 对结构的操作与内置类型相似(例如作为参数传递,作为返回值,成员赋值)。 ```cpp // 结构描述: struct inflatable { char name[20]; float volume; double price; }; // 声明结构变量 struct inflatable goose; // C需要使用关键字struct inflatable vincent; // C++可以省略关键字struct // 初始化结构变量 inflatable guest = { "jhan", 1.88, 29.99 }; // 有些实现需要用 static inflatable guest,为什么呢? inflatable duck {"yuli", 0.12, 9.98}; inflatable duck {}; // 都被设置为零 // 可以同时完成定义结构和创建结构变量的工作 struct perks { int key_number; char car[12]; } mr_smith, ms_jones; // 甚至可以同时初始化 struct perks { int key_number; char car[12]; } mr_glitz = { 7, "jhan" }; // 还可以声明没有名称的结构类型,但需要同时定义变量 struct { int x; int y; } position; /******** 结构数组 ********/ inflatable gifts[100]; // array of 100 inflatable structures inflatable guests[2] = { {"Bambi", 0.5, 21.99}, {"Godzilla", 2000, 565.99} }; // 位字段,指定占用特定位数的结构成员(使得创建与某个硬件设备上的寄存器对应的数据结构特别方便) // 字段的类型应为整型或枚举 struct torgle_register { unsigned int SN : 4; // 4 bits for SN value unsigned int : 4; // 4 bits unused bool goodIn : 1; bool goodTorgle : 1; }; torgle_register tr = {14, true, false}; ``` ## 4.5 共用体 **特点:** 1. 能够存储不同类型的数据类型,但只能同时存储其中一种类型; 2. 共用体的长度为其最大成员的长度(那还怎么节省空间?) ```cpp union one4all { int int_val; long long_val; double double_val; }; struct widgrt { char brand[20]; int type; union id { long id_num; char id_char[20]; } id_var; }; ... widget prize; ... if (prize.type == 1) cin >> prize.id_val.id_num; else cin >> prize.id_var.id_char; // 匿名共用体,共用体成员被视为变量prize的成员,它们的地址相同,不需要中间标识符id_val struct widgrt { char brand[20]; int type; union { long id_num; char id_char[20]; }; }; ... widget prize; ... if (prize.type == 1) cin >> prize.id_num; else cin >> prize.id_char; // 可以使用one4all变量来存储int、long或double,条件是在不同的时间进行 one4all pail; pail.int_val = 15; pail.double_val = 1.38; ``` **作用(使用场景):** - [ ] 疑问: 到底怎么节省的?从下面的程序运行结果来看,所占空间并没有随使用的变量数据类型而动态变化啊。常用于节省内存。当数据项使用两种或更多种格式(但不会同时使用)时,可节省空间。 ```cpp #include union test { int a; double b; }; int main() { using namespace std; test jhan; jhan.a = 4; cout << "size of int is " << sizeof(int) << endl; // 4 cout << "size of double is " << sizeof(double) << endl; // 8 cout << "size of jhan is " << sizeof(jhan) << endl; // 8 cout << "size of test is " << sizeof(test) << endl; // 8 jhan.b = 8.23; cout << "size of jhan is " << sizeof(jhan) << endl; // 8 cout << "size of test is " << sizeof(test) << endl; // 8 cin.get(); } ``` ## 4.6 枚举 **作用:** 1. 创建符号常量,代替const; 2. 枚举量是整型,可被提升为int类型,但int类型不能自动转换为枚举类型; 3. 在算术表达式中,枚举将被转换为整数,得到的类型为int,不能直接赋给枚举变量 ```cpp // 让spectrum成为新类型的名称;spectrum被成为枚举(enumeration); // 将red、orange、yellow等作为符号常量,对应整数值0~7,这些常量叫作枚举量 enum spectrum {red, orange, yellow, green, blue, violet, indigo}; enum {red, orange, yellow, green, blue, violet, indigo}; // 一些限制 spectrum band; band = blue; // valid band = 2000; // invalid band = orange + red; // invalid int color = blue; // valid, promoted to int band = 3; // invalid band = spectrum(3); // valid band = spectrum(4003); // undefined, result not sure color = 3 + red; // valid // 设置枚举量的值 enum bits{one = 1, two = 2, four = 4, eight = 8}; bits myflag; myflag = bits(6); // valid // first = 0, third = 101 enum bigstep{first, second = 100, third}; // zero, null = 0; one, numero_uno = 1 enum {zero, null = 0, one, numero_uno = 1}; // 枚举的取值范围,允许在取值范围内使用强制类型转换将整数值给枚举变量 上限:大于最大值的最小的2的幂减1 下限:若最小值不小于0,则下限为0;否则。小于最小值的最大的2的幂(加负号)加1 ``` ## 4.7 指针和自由空间 计算机程序在存储数据时必须跟踪的3种基本属性: 1. 信息存储在何处; 2. 存储的值为多少; 3. 存储的信息是什么类型。 达到这种目的的一种策略:定义简单变量,指出值得类型和符号名,让程序为值分配内存,并在内部跟踪该内存单元,**这时值是指定的量,而地址为派生量**。 **为什么要使用指针?** 使程序能够在运行阶段动态分配内存(配合new),节省空间,而不是在编译阶段就将所需的空间按可能的最大值确定下来。**这时地址为指定的量,而值为派生量**。 **特点:** 1. 改变指针所指的地址处的值,此处的变量值也变; 2. 尽管指针有许多不同的类型,但是所有指针变量本身的长度都是一样的,只是它们指向的数据类型不同; 3. 指针名可以用来加减,实现地址偏移(一次偏移一个类型长度); **注意点:** 1. 一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址,错误示范:`long * fellow; *fellow = 223323;`地址不明确,可能导致一些**最隐匿、最难以跟踪**的bug; 2. 地址本身只指出了对象存储地址的开始; 3. 内存耗尽,new引发异常; 4. new创建数组时,返回第一个元素的地址给指针; 5. 只有在使用了new后,才能使用delete,将它用于new分配的内存; 6. 不能使用sizeof运算符来确定动态分配的数组包含的字节数,但是程序确实跟踪了分配的内存量(只是信息不公用) ```cpp // 指针与地址 #include int main() { using namespace std; int a = 6; // *运算符两边的空格是可选的 // C: int *ptr; // C++: int* ptr; int * p_a; // 下面的声明创建一个指针(p1)和一个int变量(p2),对每个指针变量名,都需要使用一个* int* p1, p2 p_a = &a; /* a == *p_a &a == p_a */ *p_a = *p_a + 1; // a = 7 // 指针和数字 int * pt; pt = 0XB8000000; // type mismatch pt = (int *) 0XB8000000; // type now match // 使用new来分配内存(在运行阶段分配未命名的内存以存储值) // 常规变量声明从栈(stack)分配内存,new从堆(heap)或自由存储区(free store)分配内存 int * pn = new int; *pn = 2020; // 使用delete释放内存 int* ps = new int; int* pq = ps; cout << "ps: " << ps << endl; cout << "pq: " << pq << endl; // pd == ps delete pq; /* 如果程序只需要小型数据对象,那直接声明简单变量比使用new和指针更简单, 对于大型数据,应使用new。 在编译时为数组分配内存空间称为静态联编(static binding), 在运行时选择创建并选择长度称为动态联编(dynamic binding)。 */ // 使用new创建动态数组 // new返回第一个元素的地址,该地址被赋给指针psome int * psome = new int [10]; // 还不是占了10个int??? delete [] psome; // 指针名可以用来加减,地址偏移 double * p3 = new double [3]; p3[0] = 0.2; p3[1] = 0.5; p3[2] = 0.8; p3 = p3 + 1; // p3[0] = 0.5 p3 = p3 - 1; // p3[0] = 0.2 } ``` ## 4.8 指针、数组、指针算术 从面的例子中可以看出,我们可以像操作数组一样操作指针,原因在于指针算术和C++内部处理数组的方式。 **知识点:** 1. 整数变量加1后,其值将增加1;将指针变量加1后,增加的量等于它指向的类型的字节数; 2. C++将数组名解释为地址; 3. 对数组应用sizeof得到的是数组的长度,对指针应用sizeof得到的是指针的长度; 4. `&数组名`得到的是整个数组的地址,虽然cout输出与`数组名`的结果相同,但是它们加1后的结果不一样; 5. 数组指针:假设数组长度为20,指向数组的指针`double (*pas)[20] = &arrayName`,使用`double *pas = &arrayName`会报错;指针数组`double *pas[20]`,数组的元素是指针; 6. 在cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址; 7. 应使用strcpy()或strncpy()将字符串赋给数组,而不是赋值运算符; 8. 不能使用`=`将一个字符串赋给另一个字符数组或字符指针; **数组与指针** ```cpp #include int main() { using namespace std; double wages[3] = {100.0, 200.0, 300.0}; short stacks[3] = {3, 2, 1}; // 两种获取数组地址的方式 double * pw = wages; short * ps = &stacks[0]; // 这也说明数组的地址是第一个元素的起始地址 cout << "wages: " << wages << endl; cout << "&wages: " << &wages << endl; // 数组的地址,double (*pas)[20] = &tell cout << "wages + 1: " << wages + 1 << endl; cout << "&wages + 1: " << &wages + 1 << endl; cout << "wages[0]: " << wages[0] << endl; cout << "*wages: " << *wages << endl; cout << "*(&(wages[0])): " << *(&(wages[0])) << endl; cin.get(); /* wages: 0x70fdf0 &wages: 0x70fdf0 wages + 1: 0x70fdf8 &wages + 1: 0x70fe08 wages[0]: 100 *wages: 100 *(&(wages[0])): 100 */ } ``` **使用new创建动态结构** 创建动态结构时,不能将成员运算符句点用于结构名,因为这种结构没有名称,只是知道它的地址。这种情况有个专门的运算符`->`。 ```cpp struc things { int good; int bad; }; things grubnose = {3, 453}; things *pt = &grubnose; // grubnose.good == pt->good == (*pt).good ``` **一个使用new和delete节省内存的示例** ```cpp #include #include using namespace std; char * getname(void); int main() { char *name; name = getname(); cout << name << " at " << (int *) name << endl; delete [] name; name = getname(); cout << name << " at " << (int *) name << endl; delete [] name; cin.get(); cin.get(); return 0; } char * getname() { char temp[80]; cout << "Enter last name: "; cin >> temp; char * pn = new char[strlen(temp) + 1]; strcpy(pn, temp); return pn; } ``` **自动存储、静态存储、动态存储** C++的3中管理数据内存的方式,C++11新增了第四种类型:线程存储。 1. **自动存储** **生命周期:** 在函数内部定义的常规变量使用自动存储空间,被称为自动变量,意味着它在所属的函数被调用时自动产生,该函数结束时消亡。实际上,自动变量是一个局部变量,其作用域为包含它的代码块。 **存储位置:** 通常存储在栈中,后进先出(LIFO)。 2. **静态存储** **生命周期:** 整个程序执行期间都存在。使变量成为静态的两种方式:在函数外定义它;在声明时使用关键字static。 3. **动态存储** 使用new和delete实现。它们管理了一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap),该内存池与用于静态变量和自动变量的内存是分开的。 注意:要养成同时使用new和delete的习惯,否则容易发生内存泄漏,**C++智能指针**有助于自动完成这种任务。 **指向指针的指针** - [ ] 为什么跟结构有关的指针前面要加关键字const呢? ```cpp struct person { int age; }; person p1, p2, p3; const person * aim[3] = {&p1, &p2, &p3}; const person ** ppa = aim; auto ppb = aim; ``` ## 4.9 数组的替代品:模板类vector和array **模板类vector** `vector vt(n_elem);` 类似于string类,是一种动态数组。 ```cpp #include ... using namespace std; vector vi; // create a zero-size array of int int n; cin >> n; vector vd(n); // create an array of n doubles ``` **模板类array(C++11)** `array arr;` 注意: 1. 只有在C++11中,才可以将列表初始化用于vector和array对象,其他情况只能一个个赋值; 2. array对象和数组存储在相同的内存区域(栈),vector存储在另一区域(自由存储区或堆); 3. 不允许使用负索引,但使用中括号时C++不会检查,可以使用at成员函数来捕获非法索引。 赞赏 微信支付 支付宝支付