【C++PrimerPlus笔记】第四章 复合类型

本章介绍了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,就是枚举类型取值范围的上限。

指针和自由存储空间

指针运算符号

  1. 取址运算符&
  2. 取值运算符*

指针策略是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++将数组名解析为地址,但是数组名和指针有两个不同:

  1. 数组名是常量,指针是变量

  2. 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";

    有两个问题值得思考:

    1. 为什么flower是地址,但是cout打印了字符串内容?

      因为cout对于指向char的指针,会解释为字符串的首地址,然后继续打印后面的字符,直到遇到空字符。但是对于指向其他类型的指针,cout会直接打印地址。

    2. 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;

    访问动态结构体成员,不能使用句点运算符,因为此时不知道结构体名称,只有指向结构体的指针,有两种方式访问成员:

    1. 使用 箭头成员运算符->

      C++ 复制代码
      ps->good;
    2. 使用取值运算符,然后使用句点运算符

      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++有四种存储方式

  1. 自动存储:在代码块内部定义的常规变量使用自动存储空间,这是最常见的。自动变量是一个局部变量,作用域为包括它的代码块(一对花括号),例如上面的tmp。自动变量通常存储在栈中。
  2. 静态存储:整个程序执行期间都存在的存储方式。有两种方法,一种是在函数外面定义的变量,另一种是声明时使用关键字static
  3. 动态存储:使用newdelete管理的内存池,称为 自由存储空间 或者 堆(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的代码,编译器不会报错,但是数组已经越界。有两种方法避免:

  1. arrayvector默认不会检查越界,但是可以使用at成员方法,捕获非法索引,但是该方法会增加额外的运行时间

    C++ 复制代码
    std::array<double, 4> a2;
    a2[-2] = 4.; // 不报错
    a2.at(-2) = 4.; // 报错
  2. 借助beginend确定边界

总结

本章介绍了C++的三种复合类型,数组、结构体和指针。

  • 数组在一个数据对象存储多个同类型的值;
  • 结构体可以存放不同类型的值,通过成员关系运算符(句点)访问成员;
  • 共同体可以存放不同类型的值,但是只能存储一个值;
  • 指针是用来存储地址的变量;
  • 字符串可以使用常量、字符串数组、string类表示。

复习题

考察cin捕获整行输入、cstring操作、new/delete使用

相关推荐
无尽的大道2 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒6 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~9 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
binishuaio15 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE17 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻21 分钟前
WPF中的依赖属性
开发语言·wpf
洋24030 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙31 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点32 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
NoneCoder1 小时前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发