本章介绍了C++的三种复合类型,数组、结构体和指针。
数组初始化的三种方式
// 方式1
int array[3];
array[0] = 1;
// 方式2
int array[3] = {0,1,2};
// 方式3,C++11支持更加简明的列表初始化,省略等号
int array[3] {0,1,2};
注意,列表初始化禁止出现缩窄转换,例如下面就是不合理的
char slifs[2] {'h', 'i', 11220011}; // 11220011超过了char的表示范围0-255
以空字符\0
结尾,区别于字符数组。
两类初始化方法
// 方法1,逐个输入,以空字符结尾
char dog[3] = {'d', 'o', 'g', '\0'}
// 方法2,使用双引号
char dog[3] = "dog"
char dog[] = "dog"
注意,记得给空字符留内存。
单引号是字符常量,双引号是字符串常量,后者实际上是地址,不能直接赋值给char.
C++中任何两个由空白分隔的字符串常量都将自动被拼接为一个
cout << "this is "
"a cat";
// 方法1
#include
strlen(name1) // 输出空字符前的内容
// 方法2
sizeof(name1) // 输出name1的内存长度
使用cin
输入字符串,遇到空格字符就会停止读入,无法读取整行输入。
有两种方法可以整行读取
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()
读取换行符
C++98通过添加string
类拓展了C++库,必须在程序中包含头文件string
类设计比字符数组更加灵活,例如可以实现string对象之间的赋值(字符数组不能赋值),还可以实现python字符串一样的 拼接、合并操作。
此外,string类比字符数组更加安全,例如
#include
#include
#include
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类和字符数组的使用方式相同,可以通过cin
实现。
在整行输入时,两者使用方法不同
// 整行输入字符数组
cin.getline(charr, 30); // 句点表示法,指定数组名称和最大长度
// 整行输入string类
getline(cin, str); // istream类没有处理sring对象的方法
补充:C++11新类型,原始字符串
定义结构体
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++结构体除了成员变量,还可以有成员函数。
结构数组, 即每个元素为结构体的数组,如下所示
#include
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;
}
共用体和结构体类似,但是每次只能存储一种数据类型。
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没有名称,用在结构体中,结构体可以直接访问
struct widget{
char brand[20];
int type; // 表示union的类型
union{
long id_num;
char id_char[20];
}; // anonymous union
};
cout << widget.id_num; // 调用
另外一种创建符号常量的方式enum
enum spectrum {red, blue, yellow}; // 整型,默认从0开始
**枚举类型只有赋值运算,但是在不进行强制类型转换情况下,其他类型不能赋值给枚举类型。**枚举类型也没有定义算术运算,可能会造成错误。
枚举更常被定义符号常量,而不是创建新类型。
也可以显式设置枚举类型的值。
最初,枚举类型的取值范围仅限于声明中的值,但是C++通过强制类型转换,增加了枚举类型变量取值的合法值,计算方式为:大于枚举量声明最大值的2的幂,然后减去1,就是枚举类型取值范围的上限。
指针运算符号
指针策略是C++内存管理编程理念的核心,面向过程编程在编译阶段进行决策,而OOP强调在运行阶段进行决策,例如决定数组长度,C++采用的方法是,通过关键字new请求内存,并使用指针跟踪新分配的内存位置。
int jump = 23;
int *pe = &jump;
int *p1, *p2; // 同时声明两个int指针
int *p1, p2; // p1是int指针,p2是int类型
注意,在对指针应用取值运算符*
之前,要将指针初始化为一个确定的、适当的地址。
虽然计算机将地址当作整数处理,但是地址的算术运算是没有意义的,也不能直接将整数赋值给指针/地址,除非进行强制类型转换
int* p;
pt = (int*) 0xB8000000; // 强制类型转换
指针的真正用武之地,在于运行阶段分配未命名的内存以存储值,并通过指针访问。C使用malloc()分配内存,C++使用new分配内存,如下所示
int *pt = new int; // new后面加类型名称,返回分配内存空间的首地址
示例
#include
using namespace std;
int main()
{
int nights = 1001;
// new
int *pt = new int;
*pt = 1001;
cout << "*pt value = " << *pt << endl;
return 0;
}
C++提供了检测并处理内存失败的工具(第六章)
new和delete应该配对使用,delete只能释放new分配的内存,而且不应该尝试释放已经释放的内存块。
注意,一般不要创建两个指向同一块内存的指针,这将增加错误删除同一个内存两次的风险,如下所示
int* p1 = new int;
int* p2 = p1;
delete p1;
delete p2; // invalid, 删除同一块内存
在编译时给数组分配内存被称为静态联编(Static Binding),在运行时给数组分配内存被称为动态联编(Dynamic Binding),又被称为 动态数组(Dynamic Array),此时不用在编写时就确定数组长度。
int * da = new int [10];
delete [] da; // 删除动态数组时,应该加上[]
在访问动态数组的时候,指针和数组名使用方法类似,例如
double* p3 = new double [10];
p3 = p3 + 1; // 数组名不能加1, 但是指针可以加1
指针变量加1,增加的值等于其指向类型占用的字节数,类似于数组索引。
C++将数组名解析为地址,但是数组名和指针有两个不同:
数组名是常量,指针是变量
sizeof
数组名,返回数组占用的字节数,sizeof
指针,返回指针类型的长度,sizeof
*指针,返回指针指向数据的长度(即使是动态数组),如下所示
#include
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;
}
数组的地址
数组名被解释为第一个元素的地址,但是对数组名应用取址运算符,得到的是整个数组的地址
short tell[10];
cout << tell << endl; // 等价于 &tell[0], 数组第一个元素的地址, *short
cout << &tell << endl; // 整个数组的地址, short(*) [20]
两者虽然数值相同,但是前者+1,增加一个short类型的长度,后者+1,增加10个short类型的长度,如下所示
#include
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
也可以声明一个这种指针
short (*pas) [20]; // *pas表示pas是一个指针, short [20]表示指向的是有20个short类型的数组
// 区别于 short* pas[20];
// 此时pas优先和[]结合
给一个看上去理所当然的打印字符串代码
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
的地址,需要进行强制类型转换
cout << (int *) flower; // 强制类型转换
C-style
拷贝字符串可以使用srtcpy
或者strncpy
实现字符串拷贝,如下
#include
#include
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
创建动态结构体创建动态结构体
inflatable* ps = new inflatable;
访问动态结构体成员,不能使用句点运算符,因为此时不知道结构体名称,只有指向结构体的指针,有两种方式访问成员:
使用 箭头成员运算符->
ps->good;
使用取值运算符,然后使用句点运算符
(*ps).good;
示例
#include
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()
函数,根据输入的字符串,自动分配内存空间,然后返回分配内存的首地址
#include
#include // 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
#include
std::vector vi; // 创建空的vector
std::vector vd(n); // 创建包含n个元素的vector,这里n可以是变量或者常量,注意是圆括号
array
C++11新增模板类array
,和数组一样,长度固定,也使用栈(静态内存分配),效率更高,同时更加安全方便,比如可以将一个array对象赋值给另一个array对象。
包含头文件array
#include
std::array ad = {1.1, 2.2, 3.3, 4.4};
// 需要使用常量指定数组长度, C++11允许使用列表初始化vector和array
如果使用a1[-2]=20.2
的代码,编译器不会报错,但是数组已经越界。有两种方法避免:
array
和vector
默认不会检查越界,但是可以使用at
成员方法,捕获非法索引,但是该方法会增加额外的运行时间
std::array a2;
a2[-2] = 4.; // 不报错
a2.at(-2) = 4.; // 报错
借助begin
和end
确定边界
本章介绍了C++的三种复合类型,数组、结构体和指针。
考察cin捕获整行输入、cstring操作、new/delete使用