本章介绍了C++的三种复合类型,数组、结构体和指针。
数组
数组初始化的三种方式
            
            
              c++
              
              
            
          
          // 方式1
int array[3];
array[0] = 1;
// 方式2
int array[3] = {0,1,2};
// 方式3,C++11支持更加简明的列表初始化,省略等号
int array[3] {0,1,2};
        注意,列表初始化禁止出现缩窄转换,例如下面就是不合理的
            
            
              c++
              
              
            
          
          char slifs[2] {'h', 'i', 11220011}; // 11220011超过了char的表示范围0-255
        字符串
C-风格字符串
以空字符\0结尾,区别于字符数组。
两类初始化方法
// 方法1,逐个输入,以空字符结尾
char dog[3] = {'d', 'o', 'g', '\0'}
// 方法2,使用双引号
char dog[3] = "dog"
char dog[] = "dog"
        注意,记得给空字符留内存。
单引号是字符常量,双引号是字符串常量,后者实际上是地址,不能直接赋值给char.
拼接字符串常量
C++中任何两个由空白分隔的字符串常量都将自动被拼接为一个
            
            
              c++
              
              
            
          
          cout << "this is " 
    "a cat";
        字符串长度
// 方法1
#include<cstring>
strlen(name1)  // 输出空字符前的内容
// 方法2
sizeof(name1)  // 输出name1的内存长度
        字符串输入
使用cin输入字符串,遇到空格字符就会停止读入,无法读取整行输入。
有两种方法可以整行读取
            
            
              C++
              
              
            
          
          cin.getline(name, 20); // 数组名,数组可接受的最大长度
cin.get(name, 20) 
        cin.getline()会读取并丢弃缓冲区的换行符号,而cin.get()不会丢弃缓冲区的换行符号,因此后者在使用时,还要再加上读取换行符的过程
// 方法1 
cin.get(name, 20);
cin.get()
// 方法2
cin.get(name, 20).get()
        虽然cin.get()更麻烦,但是可以通过读取下一个字符,判断是因为换行终止,还是数组长度不够而终止,方便检查错误。
混合输入字符串和数字时,也要使用cin.get()读取换行符
string类
C++98通过添加string类拓展了C++库,必须在程序中包含头文件string
类设计比字符数组更加灵活,例如可以实现string对象之间的赋值(字符数组不能赋值),还可以实现python字符串一样的 拼接、合并操作。
此外,string类比字符数组更加安全,例如
            
            
              C++
              
              
            
          
          #include<iostream>
#include<string>
#include<cstring>
int main()
{
    using namespace std;
    char charr[30];
    string str;
    cout << "length of un-initialized array: " << strlen(charr) << endl;
    cout << "length of un-initialized string: " << str.size() << endl;
    return 0;
}
        未初始化的字符数组内容是未定义的,可能造成危险。
string类I/O
在输入单个单词时,string类和字符数组的使用方式相同,可以通过cin实现。
在整行输入时,两者使用方法不同
            
            
              C++
              
              
            
          
          // 整行输入字符数组
cin.getline(charr, 30); // 句点表示法,指定数组名称和最大长度
// 整行输入string类
getline(cin, str); // istream类没有处理sring对象的方法
        补充:C++11新类型,原始字符串
结构
定义结构体
            
            
              C++
              
              
            
          
          struct struct_name
{
	double member1; // 以分号结尾
	double member2; 
};
struct struct_name xm; // C语言中保留struct关键字
struct_name xm; // C++允许在声明结构体变量时省略关键字struct
// 初始化
struct_name xm2 = {
    12,  // 初始化、赋值时,这里不是语句,而是参数,所以用逗号
    12
}
int main()
{return 0; // C++推荐将结构体定义为外部变量}
        成员赋值,结构体可以赋值,即使成员是数组。
与C结构体不同,C++结构体除了成员变量,还可以有成员函数。
结构数组, 即每个元素为结构体的数组,如下所示
            
            
              C++
              
              
            
          
          #include<iostream>
using namespace std;
struct inflatable
{
        char name[20];
        float volume;
        double price;
};
int main()
{
        // initialize an array of structures
        inflatable guests[2] = {
                {"bambi", 0.5, 21.00},
                {"gozi", 0.4, 0.66}
        };
        return 0;
}
        共用体 Union
共用体和结构体类似,但是每次只能存储一种数据类型。
            
            
              C++
              
              
            
          
          using namespace std;
union two2all
{
        int int_name;
        double double_name;
        char char_name;
};
int main()
{
        // initialize union
        two2all a;
        cout << "bits of a: " << sizeof(a) << endl;
        cout << "input a number" << endl;
        cin >> a.int_name;
        cout << endl << "a.int_name: " << a.int_name << endl;
        cout << endl <<  "a.char_name: " <<  a.char_name << endl;
        return 0;
}
        union常见的一种用法,匿名union没有名称,用在结构体中,结构体可以直接访问
            
            
              c++
              
              
            
          
          struct widget{
    char brand[20];
    int type; // 表示union的类型
    union{
        long id_num;
        char id_char[20]; 
    }; // anonymous union
};
cout << widget.id_num; // 调用
        枚举
另外一种创建符号常量的方式enum
            
            
              C++
              
              
            
          
          enum spectrum {red, blue, yellow}; // 整型,默认从0开始
        **枚举类型只有赋值运算,但是在不进行强制类型转换情况下,其他类型不能赋值给枚举类型。**枚举类型也没有定义算术运算,可能会造成错误。
枚举更常被定义符号常量,而不是创建新类型。
也可以显式设置枚举类型的值。
最初,枚举类型的取值范围仅限于声明中的值,但是C++通过强制类型转换,增加了枚举类型变量取值的合法值,计算方式为:大于枚举量声明最大值的2的幂,然后减去1,就是枚举类型取值范围的上限。
指针和自由存储空间
指针运算符号
- 取址运算符&
 - 取值运算符*
 
指针策略是C++内存管理编程理念的核心,面向过程编程在编译阶段进行决策,而OOP强调在运行阶段进行决策,例如决定数组长度,C++采用的方法是,通过关键字new请求内存,并使用指针跟踪新分配的内存位置。
            
            
              c++
              
              
            
          
          int jump = 23;
int *pe = &jump;
int *p1, *p2; // 同时声明两个int指针
int *p1, p2; // p1是int指针,p2是int类型
        注意,在对指针应用取值运算符*之前,要将指针初始化为一个确定的、适当的地址。
虽然计算机将地址当作整数处理,但是地址的算术运算是没有意义的,也不能直接将整数赋值给指针/地址,除非进行强制类型转换
            
            
              C++
              
              
            
          
          int* p;
pt = (int*) 0xB8000000; // 强制类型转换
        使用new来分配内存
指针的真正用武之地,在于运行阶段分配未命名的内存以存储值,并通过指针访问。C使用malloc()分配内存,C++使用new分配内存,如下所示
            
            
              C++
              
              
            
          
          int *pt = new int; // new后面加类型名称,返回分配内存空间的首地址
        示例
            
            
              C++
              
              
            
          
          #include<iostream>
using namespace std;
int main()
{
        int nights = 1001;
        // new
        int *pt = new int;
        *pt = 1001;
        cout << "*pt value = " << *pt << endl;
        return 0;
}
        C++提供了检测并处理内存失败的工具(第六章)
使用delete释放内存
new和delete应该配对使用,delete只能释放new分配的内存,而且不应该尝试释放已经释放的内存块。
注意,一般不要创建两个指向同一块内存的指针,这将增加错误删除同一个内存两次的风险,如下所示
            
            
              C++
              
              
            
          
          int* p1 = new int;
int* p2 = p1; 
delete p1;
delete p2; // invalid, 删除同一块内存
        使用new创建动态数组
在编译时给数组分配内存被称为静态联编(Static Binding) ,在运行时给数组分配内存被称为动态联编(Dynamic Binding) ,又被称为 动态数组(Dynamic Array),此时不用在编写时就确定数组长度。
            
            
              C++
              
              
            
          
          int * da = new int [10];
delete [] da; // 删除动态数组时,应该加上[]
        在访问动态数组的时候,指针和数组名使用方法类似,例如
            
            
              C++
              
              
            
          
          double* p3 = new double [10];
p3 = p3 + 1; // 数组名不能加1, 但是指针可以加1
        指针算术
指针变量加1,增加的值等于其指向类型占用的字节数,类似于数组索引。
C++将数组名解析为地址,但是数组名和指针有两个不同:
- 
数组名是常量,指针是变量
 - 
sizeof数组名,返回数组占用的字节数,sizeof指针,返回指针类型的长度,sizeof*指针,返回指针指向数据的长度(即使是动态数组),如下所示C++#include<iostream> using namespace std; int main() { double wages[3] = {1,2,3}; double* pd = wages; cout << "sizeof(wages): " << sizeof(wages) << endl; cout << "sizeof(pd): " << sizeof(pd) << endl; cout << "sizeof(*pd): " << sizeof(*pd) << endl; double* p2 = new double [3]; cout << "sizeof(dynamic array): " << sizeof(p2) << endl; cout << "sizeof(*p2) :" << sizeof(*p2) << endl; return 0; }数组的地址
数组名被解释为第一个元素的地址,但是对数组名应用取址运算符,得到的是整个数组的地址
C++short tell[10]; cout << tell << endl; // 等价于 &tell[0], 数组第一个元素的地址, *short cout << &tell << endl; // 整个数组的地址, short(*) [20]两者虽然数值相同,但是前者+1,增加一个short类型的长度,后者+1,增加10个short类型的长度,如下所示
C++#include<iostream> using namespace std; int main() { short tell[10]; cout << "tell: " << tell << endl; cout << "tell+1: " << tell + 1 << endl; cout << "tell: " << &tell << endl; cout << "tell+1: " << &tell + 1 << endl; return 0; }输出
tell: 0x7ffff9c1ad10 tell+1: 0x7ffff9c1ad12 &tell: 0x7ffff9c1ad10 &tell+1: 0x7ffff9c1ad24也可以声明一个这种指针
C++short (*pas) [20]; // *pas表示pas是一个指针, short [20]表示指向的是有20个short类型的数组 // 区别于 short* pas[20]; // 此时pas优先和[]结合指针和字符串
给一个看上去理所当然的打印字符串代码
C++char flower[10] = "rose"; cout << flower << "s are red\n";有两个问题值得思考:
- 
为什么flower是地址,但是
cout打印了字符串内容?因为
cout对于指向char的指针,会解释为字符串的首地址,然后继续打印后面的字符,直到遇到空字符。但是对于指向其他类型的指针,cout会直接打印地址。 - 
cout打印"s are red\n"是什么原理?在C++中,用引号括起来的字符串和数组名一样,会被解释为第一个元素的地址。
 
总结:在
cout和多数C++表达式中,char数组名、char指针、用引号括起来的字符串都会被解释为字符串第一个元素的地址,与传递整个字符串相比,减少了工作量。如果想用
cout打印指向char的地址,需要进行强制类型转换C++cout << (int *) flower; // 强制类型转换C-style拷贝字符串可以使用
srtcpy或者strncpy实现字符串拷贝,如下C++#include<iostream> #include<cstring> int main() { using namespace std; // claim an array char animal[20] = "tiger"; cout << animal << " at " << (int*) animal << endl; // get new storage char* ps = new char[strlen(animal) + 1]; // copy string to new storage using strcpy strcpy(ps, animal); cout << ps << " at " << (int*) ps << endl; return 0; }strcpy有点危险,因为字符串长度可能超过数组长度,为避免这个问题,需要使用strncpy,接收第三个参数,即要复制的最大字符数。使用
new创建动态结构体创建动态结构体
c++inflatable* ps = new inflatable;访问动态结构体成员,不能使用句点运算符,因为此时不知道结构体名称,只有指向结构体的指针,有两种方式访问成员:
- 
使用 箭头成员运算符
->C++ps->good; - 
使用取值运算符,然后使用句点运算符
C++(*ps).good; 
示例
C++#include<iostream> struct inflatable{ char name[20]; float volume; double price; }; int main(){ using namespace std; inflatable* ps = new inflatable; cout << "Enter name of inflatable item: "; cin.get(ps->name, 20); cout << "Enter volume in cubic feet: "; cin >> (*ps).volume; cout << "Name: " << (*ps).name << endl; cout << "Volume: " << ps->volume << endl; return 0; }两者的区别,如果结构标识符是结构名,则使用句点运算符,如果标识符是指向结构的指针,则使用箭头运算符。
**任务:**设计一个
getname()函数,根据输入的字符串,自动分配内存空间,然后返回分配内存的首地址C++#include<iostream> #include<cstring> // strlen using namespace std; char* getname(void){ char tmp[80]; cout << "Enter your name: "; cin.get(tmp, 80); char* pn = new char[strlen(tmp) + 1]; // +1 means \0 strncpy(pn, tmp, 80); return pn; } int main(){ char* name = getname(); cout << name << " at " << (int*)name << endl; delete [] name; // delete dynamic name!! return 0; } - 
 
自动存储、静态存储和动态存储
根据用于分配内存的方法,C++有四种存储方式
- 自动存储:在代码块内部定义的常规变量使用自动存储空间,这是最常见的。自动变量是一个局部变量,作用域为包括它的代码块(一对花括号),例如上面的
tmp。自动变量通常存储在栈中。 - 静态存储:整个程序执行期间都存在的存储方式。有两种方法,一种是在函数外面定义的变量,另一种是声明时使用关键字
static - 动态存储:使用
new和delete管理的内存池,称为 自由存储空间 或者 堆(heap),数据的声明周期不完全受程序或函数的生存时间控制。 
数组替代品
模板类vector
vector类也是一种动态数组,使用new和delete管理内存,功能强大,但是效率较低。
需要导入头文件vector
            
            
              C++
              
              
            
          
          #include<vector>
std::vector<int> vi; // 创建空的vector
std::vector<double> vd(n); // 创建包含n个元素的vector,这里n可以是变量或者常量,注意是圆括号
        模板类array
C++11新增模板类array,和数组一样,长度固定,也使用栈(静态内存分配),效率更高,同时更加安全方便,比如可以将一个array对象赋值给另一个array对象。
包含头文件array
            
            
              c++
              
              
            
          
          #include<array>
std::array<double, 4> ad = {1.1, 2.2, 3.3, 4.4}; 
// 需要使用常量指定数组长度, C++11允许使用列表初始化vector和array
        数组越界检查
如果使用a1[-2]=20.2的代码,编译器不会报错,但是数组已经越界。有两种方法避免:
- 
array和vector默认不会检查越界,但是可以使用at成员方法,捕获非法索引,但是该方法会增加额外的运行时间C++std::array<double, 4> a2; a2[-2] = 4.; // 不报错 a2.at(-2) = 4.; // 报错 - 
借助
begin和end确定边界 
总结
本章介绍了C++的三种复合类型,数组、结构体和指针。
- 数组在一个数据对象存储多个同类型的值;
 - 结构体可以存放不同类型的值,通过成员关系运算符(句点)访问成员;
 - 共同体可以存放不同类型的值,但是只能存储一个值;
 - 指针是用来存储地址的变量;
 - 字符串可以使用常量、字符串数组、string类表示。
 
复习题
考察cin捕获整行输入、cstring操作、new/delete使用