《C++ Primer Plus》:复合类型

本章内容概览:

  • 数组
  • C风格字符串
  • string类字符串
  • getline()和get()读取字符串
  • 结构体
  • 共用体
  • 枚举
  • 指针
  • new和delete管理动态内存
  • 动态数组
  • 动态结构
  • 自动存储、静态存储和动态存储
  • vector和array简述

数组

要创建数组可使用声明语句,数组声明应指出一下三点:

  • 存储在每个元素中的值的类型
  • 数组名
  • 数组中的元素数

例如:

short months[12];

通用格式为:

typeName arrayName[arraySize];

arraySize必须是整型常数或const值,也可以是常量表达式(如sizeof()),具体来说就是不能使用变量,这是因为常量的值是在编译时已知的,而变量值是在运行时设置的,不过使用new来动态设置可以避开这种限制。

C++允许使用列表初始化数组:

int yamcosts = {20, 30, 5}

如果没有初始化函数中定义的数组,则其元素值是不确定的,这意味着元素的值为以前驻留在内存单元的值。

只有在定义数组时才能使用初始化,同时不能使用赋值语句直接将一个数组赋给另一个数组:

int cards[4] = {3,6,8,10};  //valid
int hand[4];  //valid
hand[4] = {5,6,7,9};  //invalid
hand = cards;  //invalid

如果只对数组的一部分初始化,剩下的元素会用零来填充。

float totals[500] = {0};  // all the elements are 0 

如果初始化数组时方括号内不填常量,那么C++自动根据后面的元素数计算元素个数:

short things[] = {1,5,3,8};  //size: 4

C++11的列表初始化可以省略等号:

double earnings[4] {1,2,3,4};

同时可以在大括号中省略元素来全部零初始化:

float balances[100] {};

同时,列表初始化禁止缩窄转换:

long a[] = {25,92,3.0};  //in valid
char b[4] = {'a','b',123456,'\0'};  //invalid

C++STL提供了数组的替代品——vector,更加的灵活,C++11还提供了array。

字符串

C++有两种字符串:C风格字符串和strinig类型字符串。

C风格字符串用char数组定义,以空字符即'\0'结尾,如:

char b[4] = {'a','b','c','\0'}; 

不过这种方式很麻烦,我们可以使用双引号来定义字符串,这种字符串成为字符串常量或字符串字面值:

char bird[11] = "Mr. Cheeps";  //未初始化位置处用'\0'填充
char fish[] = "Bubbles";

用括号扩起的字符串隐式地包括结尾地空字符,所以不用特地包括。

字符串常量和字符常量不能互换,如:

char shirt_size = 'S';  //valid
//-----------------------
char shirt_size = "S";  //invalid

因为下面的语句尝试将一字符串地址赋给shirt_size,C++编译器不允许这一操作。

字符串操作

字符串拼接时,前一个字符串的末尾空字符会被下一个字符串的首字符代替。

sizeof()方法可以获得数组长度,而<cstring>头文件中的strlen()方法可以获得字符串的长度,而不是数组本身的长度,不考虑空字符。

字符串输入

字符串输入时,我们考虑使用cin:

char name[20];
char dessert[20];

cin << name;  // 输入Alistair Dreed
cin << dessert;  

使用上述这种方式会发现,如果我们在输入名字时包括空格,我们来不及对第二个字符串输入进行操作时程序就结束了。由于我们不能输入空字符,所以cin使用空白(空格,制表和换行)来确定字符串的结束位置,所以说cin只读取了Alistari,就将其存储在name数组中了,并在末尾添加空字符,然后Dreed放在输入队列中,然后进入下一个cin时,直接使用输入队列中的Dreed,并将其存储在dessert中,并在末尾添加空字符。

还有的问题是,如果输入字符串长度过长,那么就很难存储在数组中。

因此,我们考虑改为每行读取,cin对象的getline()和get()方法可以实现这个目的,这两个函数都读取一行输入,直到换行符,getline()会丢弃换行符,将其转为一个空字符,get()将换行符保留在输入序列中。

cin.getline()的第一个参数是用来存储字符串的数组名,第二个参数是读取的字符数,同时getline()会自动在末尾填充空字符,在获得所需字符数后会停止读取。如:

cin.getline(name, 20);

cin.get()参数和用法与getline()一致,只是将换行符保留在输入队列中,如:

cin.get(name, 20);
cin.get(dessert, 20);

第一次输入并以回车结尾后,第二次输入的第一个字符遇到的是换行符,此时认为达到末尾,则不会读取内容。不过cin.get()有一种变体,不带参数的cin.get()可以读取下一个字符,所以下面的代码是可用的:

cin.get(name, 20);
cin.get();
cin.get(dessert, 20);

另外,还可以将两个方法拼接起来:

cin.get(name, 20).get();

这是因为cin.get()方法会返回一个cin对象。

如果遇到空行的话,get()方法会设置失效位,接下来的输入会阻断,可以用cin.clear()来恢复输入。

如果输入字符数量多余指定的空间,getline()和get()会把余下的字符留在输入队列中,getline()还会设置失效位,阻断接下来的输入。

string类

string类在std名称空间中。

  • 可以使用C风格字符串来初始化string对象
  • 可以使用cin来将输入存储到string对象
  • 可以使用cout来显示string对象
  • 可以使用数组表示法访问string对象中的字符

string对象创建方式:

std::string str1;
std::string str2 = "panther";

同样string对象也可以使用列表初始化:

std::string str {"lalala"};

数组间不能直接赋值,但string对象可以直接赋值:

std::string str1;
std::string str2 = "panther";
str2 = str1;

string字符串合并可以直接加在一起:

std::string str3;
str3 = str1 + str2;

string类的输入可以使用cin,也可以使用getline(cin, str),getline()是一个类方法,用于string对象的输入。

结构体

结构体样式:

struct inflatable
{
    char name[20];
    float volume;
    double price;
};

定义结构体后就可以使用结构体名称定义结构体变量:

inflatable hat;

C++定义结构体变量可以省略struct。

定义后就可以使用.运算符来访问成员:

hat.name;
hat.volume;
hat.price;

同样C++11还可以使用列表初始化:

inflatable duck {"Daphne",  0.12, 9.98};

另外,还可以使用赋值运算符将结构赋给另一个同类型的结构体,这被称为成员赋值:

inflatable duck {"Daphne",  0.12, 9.98};
inflatable cat;
cat = duck;

还可以同时完成结构体定义和变量声明:

struct perks
{
    int key_number;
    char cat[12];
} mr_smith, ms_jones;

还可以像下面这样加上结构体变量初始化:

struct perks
{
    int key_number;
    char cat[12];
} mr_glitz =
{
    7,
    "Packard"
};

同时还可以省略结构体名称,只是之后不能创建同类结构体:

struct
{
    int x;
    int y;
}position;

结构体也可以作为一个类型用来创建数组,同时使用列表初始化:

inflatable gifts[2]=
{
    {"Bambi", 0.5, 21.99},
    {"Godzilla", 2000, 565.99}
};

共用体

共用体可以存储不同的数据类型,但只能同时存储其中的一种类型,即结构体可以同时存储int、long、double,共用体只能存储int或long或double:

union one4all
{
    int int_val;
    long long_val;
    double double_val;
};

这样使用:

one4all pail;
pail.int_val = 15;
pail.double_val = 1.38;  //lost int_val

共用体每次只能存储一个值,因此必须有足够的空间来存储空间最大的成员。

枚举

枚举可用来替代const常量,定义:

enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};

spectrum作为新类型名称,red~ultrviolet作为符号常量,存储的整型值对应0-7。使用这个枚举名称创建枚举变量:

spectrum band;

在不进行强制转换的情况下,只能将枚举常量赋给该变量:

band = blue;  //valid
band = 2000;  //invalid

枚举只能使用赋值运算符:

band = orange;  //valid
++band;  //invalid

枚举量可以被提升为int类型,反之不能自动完成:

int color = blue;  //valid
band = 3;  //invalid

可以使用强制转换将int值转化为枚举类型:

band = spectrum(3);

枚举值在定义时可以显式赋值:

enum bite {one = 1, two = 2, four = 4, eight = 8};

可以只指定部分值:

enum bigstep {first, second = 100, third};

第一个值默认为0, second后面的值默认比前面的值大1.

还可以创建多个值相同的枚举量:

enum {zero,null = 0, one, number_uno = 1};

这样,zero为0,one为1。

指针和自由存储空间

指针是一个变量,其存储的是值得地址,而不是值本身。使用&运算符可以获得变量的地址。

使用常规变量时,值是指定的量,而地址为派生量。

面向对象编程和传统的过程式编程的区别在于,OOP强调在运行阶段进行决策,而后者在编译阶段决策。例如,考虑对数组分配内存的情况,传统的方法是声明一个数组,在C++中,必须指定数组的长度,这在编译时就决定好了,假如大多数情况下我们只需要20个元素,但有时我们需要200个元素,这样就必须定义200长度的数组,很显然,这浪费了许多内存,而OOP将这一决策推迟到运行阶段,可以动态地告诉程序需要多少元素。C++采用的策略是使用new来请求正确数量的内存,并使用指针来跟踪新分配的内存的位置。

就像上面提到的新策略,变量将地址视为指定的量,将值视为派生量,这就是指针。可以使用*运算符获得指针存储地址处的值。例如:

int a = 6;
int* p_a;
p_a = &a;
cout << *p_a;  //6

声明指针使用类型加*,如int*被称为指向int的指针,是一种复合类型。

注意C++在创建指针时,计算机分配用来存储地址的内存,但不会分配用来存储指针所指向数据的内存,如下面是不合法的:

long* fellow;
*fellow = 223323;

fellow创建后,不知道它指向哪里,那么223323就不知道存放在哪里了,所以其可能存储在任何地址处。

一定要在使用*前让指针指向某一值的地址。

new

C++可以使用new来为指针分配内存:

int* pn = new int;

new将找到指定类型的长度正确的内存块,并返回该内存块的地址。
比较一下传统方法:

int higgens;
int* pt = &higgens;

这一方法的指针指向了某一具体变量的地址,但使用new来分配内存的话,指针指向的是某一未命名的内存,这一内存可被称为数据对象。

delete

在用new为指针分配内存后,需要在合适的时候使用delete释放内存空间:

int* ps = new int;
...
delete ps;

delete只会释放ps指向的内存,但不会删除ps。如果不使用delete,可能会发生内存泄露。

用new创建动态数组

例如:

int* psome = new int[10];

这将创建一个包含10个元素的数组,new运算符返回该数组地址,并赋予psome指针,同时最后需要释放该内存:

delete[] some;

psome指向数组的首地址。同时,不能使用sizeof运算符来确定动态分配的数组包含的字节数。

创建动态数组通用格式:

type_name* pointer_name = new type_name[num_elements];

我们可以直接使用[]来使用这一动态数组,如psome[1],即C和C++中数组和指针大致等价。

我们不能修改数组名的值,但可以修改指针的值,对于psome,它是数组首地址,如果psome+1,那么就是数组第二个元素的地址,以此类推,也就是指针加减对应指向类型的字节数。

指针、数组和指针运算

指针运算就如我们上面所说的,同时,数组在C++中被解释为地址,数组名解释为数组首元素地址,即:

int[10] =wages;
int* pw = wages;  // 将数组首地址赋予指针
wages = &wages[0];  // 等价

还是上面的数组,对于wages[1],C++解释为*(wages+1),即首地址加上字节数后获得第二个元素地址,然后*取值,依此类推。但对于sizeof运算符,C++不将数组名解释为地址。

对数组取地址时,数组名也不会被解释为其地址:数组名被解释为首地址,&数组名被解释为整个数组的地址。对于short tell[10]tell+1将首地址加2,&tell+1将地址加20,换句话说,tell*short,'&tell'是short(*)[20],即指向包含20个元素的short数组。>

使用new创建动态结构体

inflatable* ps = new inflatable;

创建很简单,C++为这结构体指针提供了->运算符来访问成员。

ps是指向结构的指针,*ps是被指向值本身——结构本身,所以可以(*ps).price这么使用。

自动存储、静态存储和动态存储

  1. 自动存储
    在函数内部定义的常规变量使用自动存储空间,被称为自动变量,即在函数调用时创建,函数结束时消亡。换句话说,自动变量是一个局部变量,作用域是包含它的代码块。自动变量常存储在栈中,按后进先出的方式。

  2. 静态存储
    静态存储是整个程序执行期间都存在的存储方式。有两种方式:在函数外定义;使用static关键字声明。

static double fee = 56.50;
  1. 动态存储
    使用new和delete运算符。它们管理一个内存池,这在C++中称为自由存储空间或堆,和静态变量、自动变量的内存池分开。我们可以在一个函数中new,另一个函数中delete。

栈、堆和内存泄漏:如果使用new运算符创建内存空间后,没有调用delete,即使包含指针的内存由于作用域规则和对象声明周期的原因被释放,在自由存储空间上的动态分配的变量或结构也存在着,由于无法访问,即指向这些内存的指针无效了,这将导致内存泄漏,被泄露的内存在程序的整个声明周期内都不可使用,最糟糕的情况就是内存耗尽,无法继续分配内存。
C++之后添加了智能指针,它可以自动管理内存,在合适的时候释放内存。如果C++使用者不能确定何时使用delete,建议使用智能指针,只是占用的内存大一点。

数组替代品

vector

vector是一种动态数组,自动使用new和delete来管理内存。创建一个vector对象的方式:

std::vector<typeName> vt(n_elem);

n_elem可以是0,同时可以使用索引[]来访问元素。

array

使用vector效率稍低,如果是固定长度数组,使用数组更好,但不安全,这时候可以使用array。array对象长度固定,也是用栈(静态内存分配),和数组效率相同,但更方便安全。创建一个array对象方式:

array<typeName, n_elem> arr;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容