本章介绍了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使用