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

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

字符串

C-风格字符串

以空字符\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()读取换行符

string类

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类I/O

在输入单个单词时,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;
}

共用体 Union

共用体和结构体类似,但是每次只能存储一种数据类型。

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

指针和自由存储空间

指针运算符号

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

指针策略是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; // 强制类型转换

使用new来分配内存

指针的真正用武之地,在于运行阶段分配未命名的内存以存储值,并通过指针访问。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++提供了检测并处理内存失败的工具(第六章)

使用delete释放内存

new和delete应该配对使用,delete只能释放new分配的内存,而且不应该尝试释放已经释放的内存块。

注意,一般不要创建两个指向同一块内存的指针,这将增加错误删除同一个内存两次的风险,如下所示

int* p1 = new int;
int* p2 = p1; 
delete p1;
delete p2; // invalid, 删除同一块内存

使用new创建动态数组

在编译时给数组分配内存被称为静态联编(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++将数组名解析为地址,但是数组名和指针有两个不同:

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

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

    有两个问题值得思考:

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

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

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

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

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

      ps->good;
      
    2. 使用取值运算符,然后使用句点运算符

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

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

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

    std::array a2;
    a2[-2] = 4.; // 不报错
    a2.at(-2) = 4.; // 报错
    
  2. 借助beginend确定边界

总结

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

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

复习题

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

你可能感兴趣的:(c++,开发语言)