C++ primer plus笔记 --- 第4章、复合类型

四、复合类型

4.1、数组

在C++中,相同数据类型的元素集合。数组大小确定,无法动态改变。数组元素的访问通过下标操作实现,下标从0开始计数。

数组的定义格式为:

type arrayName[arraySize];

type表示数组中元素的数据类型,arrayName表示数组的名称,arraySize表示数组的大小。

例如,定义一个包含10个整数的数组:

int nums[10];

通过下标操作来访问数组中的元素:

nums[0] = 1;
nums[1] = 2;

上述代码将数组的第一个元素赋值为1,第二个元素赋值为2。

C++还支持多维数组。例如,以下代码定义了一个包含3行4列的二维数组:

int matrix[3][4];

可以使用两个下标来访问二维数组中的元素,例如:

matrix[0][0] = 1;
matrix[1][2] = 3;

上述代码将二维数组的第一个元素(即第一行第一列)赋值为1,第4个元素(即第二行第三列)赋值为3。

数组也可以被初始化为指定的初始值。例如,以下代码定义了一个包含3个整数的数组,并初始化为1、2、3:

int nums[3] = {1, 2, 3};

如果没有显式指定初始值,则数组元素会被默认初始化为0。例如,以下代码定义了一个包含5个整数的数组,并默认初始化为0:

int nums[5] = {};

在C++11及以后的版本中,还支持使用花括号来进行初始化。例如:

int nums[] {1, 2, 3};

上述代码会自动推断数组大小为3,同时将数组初始化为1、2、3。

需要注意的是,数组的大小必须是一个常量表达式,例如:

const int SIZE = 10;
int nums[SIZE];

使用变量来表示数组的大小是不合法的,例如:

int size = 10;
int nums[size]; // 错误:size不是常量表达式

如果需要动态分配内存,应该使用动态数组(vector)或new运算符来实现。

4.1.1、程序说明(后续补充)

4.1.2、数组的初始化规则

数组可以定义时进行初始化,也可以使用赋值语句对数组进行初始化。C++的几种常用数组初始化方式。

1、不指定元素个数,由编译器根据初始值个数来确定:

int nums[] = {1, 2, 3}; // 数组大小为3

2、指定元素个数,但初始值的个数少于指定的元素个数,则会自动用0来补齐。

int nums[4] = {1, 2}; // [1, 2, 0, 0]

3、可以按照顺序指定数组元素的初始值:

int nums[5] = {1, 2, 3, 4, 5};

4、初始化二维数组时,使用花括号括起多个一维数组,每个一维数组可以用花括号括起来指定初值。

int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};

需要注意的是,数组的大小必须是常量表达式,不能使用变量。如果需要动态分配内存,建议使用vector或new运算符等动态数据结构。

笔记:数组大小不能使用变量,这点非常重要,以后进行相关的操作时,一定要注意这一点。

4.2、字符串

字符串是由字符组成的一串字符序列。C++标准库提供了string类,用于处理字符串相关的操作。在使用string类之前,需要包含头文件(别忘了!!!!!!)

1、字符串的定义和初始化

使用string类可以很方便地定义和初始化字符串。以下是一些常见的定义方式:

string s1;                       // 定义一个空字符串
string s2 = "hello";             // 定义并初始化一个字符串
string s3(10, 'a');              // 定义一个由10个'a'字符组成的字符串

第三种方式通过构造函数实现字符串的初始化,第一个参数是字符串的长度,第二个参数是用于初始化的字符

笔记:学到了,尤其是第三种方法,区别于其他类似C语言的写法。

2、字符串的基本操作

string类提供了许多常用的字符串操作,例如:

  • 获取字符串的长度:使用size()或length()成员函数获取字符串的长度。
  • 拼接字符串:可以使用加号运算符(+)或append()成员函数拼接字符串。
  • 截取字符串:可以使用substr()成员函数截取子串。
  • 查找子串:可以使用find()或rfind()成员函数查找子串是否存在。
  • 替换子串:可以使用replace()成员函数将子串替换为新的字符串。
  • 比较字符串:可以使用比较运算符(==, !=, <, <=, >, >=)或compare()成员函数比较两个字符串的大小关系。

以下是一些使用string类的示例:

string s = "hello";
cout << s.size() << endl; // 输出5
cout << s + ", world!" << endl; // 输出hello, world!
s.append(" world!");
cout << s << endl; // 输出hello world!
cout << s.substr(6, 5) << endl; // 输出world
cout << (s.find("world") != string::npos) << endl; // 输出1表示找到,0表示未找到
s.replace(6, 5, "China");
cout << s << endl; // 输出hello China!
cout << s.compare("hello") << endl; // 输出1表示大于,0表示相等,-1表示小于

需要注意的是,在使用string类时,由于其内部实现是动态分配内存,因此不用担心字符串的长度超出固定限制。

4.2.1、拼接字符串常量

在C++中,可以使用加号运算符(+)append()成员函数拼接字符串。但需要注意的是,在拼接字符串与字符串常量时,需要将字符串常量转换为字符串类型。

有以下几种方式可以将字符串常量转换为字符串类型:

1、使用string类的构造函数:

string s1 = "hello";
string s2 = string(" world!");
string s3 = s1 + s2; // 将s1和s2拼接成一个新的字符串

在上述示例中,string类的构造函数可以将字符串常量转化为字符串类型,然后使用加号运算符拼接字符串。

2、使用C风格字符串函数:

const char* s1 = "hello";
const char* s2 = " world!";
char s3[100] = {0};
strcpy(s3, s1); // 将s1拷贝到s3中
strcat(s3, s2); // 将s2拼接到s3中

在上述示例中,使用了C风格字符串函数strcpy()和strcat()来实现字符串常量和字符串类型之间的转换和拼接。

需要注意的是,使用C风格字符串函数时,需要手动管理内存,容易出现内存泄漏等问题,因此建议尽量使用string类

4.2.2、在数组中使用字符串

在C++中,可以使用字符数组来存储和处理字符串。字符数组是由一组字符组成的一维数组,可以用于存储字符串。要在字符数组中存储字符串,需要将字符数组的最后一个元素设为’\0’,即空字符,表示字符串的结束。例如:

char str[] = "hello, world!"; // 字符串长度为14,字符数组长度为15(包括空字符)

上述代码定义了一个包含15个元素的字符数组,其中前14个元素存储字符串,最后一个元素为’\0’。可以使用下标操作访问字符数组中的元素,例如:

cout << str[0] << endl; // 输出'h'
str[0] = 'H'; // 将字符串第一个字符改为大写

C++提供了许多常用的字符数组处理函数,例如:

  • strlen()函数:用于获取字符串的长度(不包括末尾的空字符)。
  • strcpy()函数:用于将一个字符串复制到另一个字符串中。
  • strcat()函数:用于将一个字符串拼接到另一个字符串的末尾。
  • strcmp()函数:用于比较两个字符串的大小关系。

以下是一个使用字符数组处理字符串的示例:

char str1[] = "hello,";
char str2[] = " world!";
char str3[100] = {0};
strcpy(str3, str1); // 将str1复制到str3中
strcat(str3, str2); // 将str2拼接到str3末尾
cout << strlen(str3) << endl; // 输出12
cout << strcmp(str1, str2) << endl; // 输出-1表示str1小于str2

需要注意的是,在使用字符数组处理字符串时,需要手动管理字符数组的内存,避免越界访问和内存泄漏等问题。建议尽量使用string类来处理字符串。

4.2.3、字符串输入

在 C++ 中输入字符串可以通过以下两种方式实现。

1、使用 std::getline() 函数

标准库中的 std::getline() 函数可以从标准输入流(std::cin)中读取一行字符串,并将其保存到字符串对象(std::string)中。函数原型为:

std::getline(std::istream& is, std::string& str, char delim = '\n');

其中:

  • is:输入流,一般为 std::cin
  • str:用来存储读取的字符串的 std::string 对象。
  • delim:可选参数,用来指定读取字符串时的分隔符,默认为换行符(\n)。如果在读取过程中遇到此分隔符,就停止读取并将其丢弃。

示例代码:

#include 
#include 

int main() {
    std::string str;
    std::cout << "Please enter a string: ";
    std::getline(std::cin, str);
    std::cout << "You entered: " << str << std::endl;
    return 0;
}

输出:

Please enter a string: hello, world!
You entered: hello, world!

2、使用 std::cin 输入运算符 (>>)

可以使用 >> 运算符从标准输入流(std::cin)中逐个单词地读取字符串(遇到空格或制表符(\t)就停止),并存储到标准字符串(std::string)变量中。例如:

#include 
#include 

int main() {
    std::string str;
    std::cout << "Please enter a string: ";
    std::cin >> str;
    std::cout << "You entered: " << str << std::endl;
    return 0;
}

输出:

Please enter a string: hello, world!
You entered: hello,

std::cin 在读取字符串时,会将空格、制表符等作为分隔符,只读取空格/制表符前面的部分,而忽略后面的部分。所以在上面的示例代码中,只读取到了 “hello,”,而后面的 “world!” 被留在了输入流中。

需要注意的是,如果使用 >> 运算符读取字符串时,输入流(std::cin)中的空格/制表符等分隔符会继续存在于输入流中,可以使用 std::cin.ignore() 函数清除它们。例如:

#include 
#include 

int main() {
    std::string str;
    std::cout << "Please enter a string: ";
    std::cin >> str;
    std::cin.ignore(std::numeric_limits::max(), '\n'); // 清除输入流中的分隔符
    std::cout << "You entered: " << str << std::endl;
    return 0;
}

输出:

Please enter a string: hello, world!
You entered: hello, world!

在调用 std::cin.ignore() 函数时,第一个参数指定要从输入流中读取的最大字符数,第二个参数指定要忽略的分隔符。std::numeric_limits::max() 表示 std::streamsize 类型的最大值,即忽略输入流中的所有字符。

4.2.4、每次读取一行字符串输入

在 C++ 中,可以使用标准库中的 std::getline() 函数来读取一行字符串输入。可以在循环中反复调用 std::getline() 函数以读取多行字符串。以下是示例代码:

#include 
#include 

int main() {
    std::string line;
    while (std::getline(std::cin, line)) {
        std::cout << "You entered: " << line << std::endl;
    }
    return 0;
}

在上述示例代码中,循环读取标准输入流(std::cin)中的字符串,将读取到的字符串保存到 line 变量中,并输出字符串。

需要注意的是,std::getline() 函数读取到的字符串并不包含行结束符(\n),因此需要在输出时手动添加。另外,当输入结束时,可以使用 std::cin.eof() 函数检查是否到达输入流的末尾。例如:

#include 
#include 

int main() {
    std::string line;
    while (std::getline(std::cin, line)) {
        std::cout << "You entered: " << line << std::endl;
    }
    if (std::cin.eof()) {
        std::cout << "Input ended." << std::endl;
    }
    return 0;
}

在上述示例代码中,当输入结束时,输出 “Input ended.”。

4.2.5、混合输入字符串和数字

在 C++ 中,要支持混合输入字符串和数字,可以使用串流操作符>>。如果要从标准输入读取一行中同时包含字符串和数字,可以通过以下方式:

#include 
#include 

int main() {
    std::string str;
    int num;
    std::cout << "Please enter a string and a number: ";
    std::cin >> str >> num;
    std::cout << "You entered: " << str << " " << num << std::endl;
    return 0;
}

在上述示例代码中,std::cin 从标准输入中读取一行,按照空格或制表符来分离字符串和整数,存储到 str 和 num 中。需要注意的是,这个方法要求输入中字符串和数字之间必须有空格或制表符分隔,否则会出现错误。以下为示例输出:

Please enter a string and a number: hello 123
You entered: hello 123

如果在输入中混合了多个数字,则可以使用循环逐个读取。以下为示例代码:

#include 
#include 

int main() {
    std::string str;
    int num;
    std::cout << "Please enter a string and multiple numbers (end input with a non-numeric character): ";
    std::cin >> str;
    while (std::cin >> num) {
        std::cout << "You entered: " << str << " " << num << std::endl;
    }
    return 0;
}

在上述示例代码中,先输入字符串 str,然后使用循环按照空格或制表符分隔输入,将多个数字分别存储到 num 中并输出。当输入一个非数字时,循环结束。需要注意的是,在处理非数字时,需要清除输入流中的标记和余下的输入,否则会影响后续的输入操作。可以通过以下代码来清除输入流:

std::cin.clear(); // 清除输入流标记
std::cin.ignore(std::numeric_limits::max(), '\n'); // 清除输入流中的剩余字符

其中,std::cin.clear() 用于清除输入流标记,而 std::cin.ignore() 用于清除输入流中的剩余字符,通过忽略流的最大字符数和换行符来实现。


4.3、string类简介

4.3.1、C++11字符串初始化

头文件:string 类,必须包含头文件string,string类位于名称空间中。

两种风格:C++ 11字符串初始化,注意C/C++两种风格

char charr1[20];            // C语言风格
char charr2[20] = "jaguar"; // C语言风格
string str1;                // C++风格
string str2 = "sadasd";     // C++风格
string str3 = "hello";      // C++风格

C语言风格不能将一个数组赋值给另一个数组,但可以将一个string对象赋值给另一个string对象。

4.3.2、赋值、拼接和附加

string赋值、拼接、附加:string可以用+将两个对象进行相加,使用+=将一个string对象附加到另一个的末尾。

char charr1[20];
char charr2[20] = "jaguar";
string str1;
string str2 = "sadasd";
string str3 = "hello";
str1 = str2;
    
str3 += str2;

cout << "str1 = " << str1 << endl;
cout << "str2 = " << str2 << endl;
cout << "str3 = " << str3 << endl;

对应的运行结果:

str1 = sadasd
str2 = sadasd
str3 = hellosadasd

可以看到,使用+=是直接拼接到末尾,不会添加空格。

4.3.3、string类的其他操作

string类其他的操作(C语言库和C++库的对比)

1、C风格字符串操作

#include 
#include 
using namespace std;

int main()
{
    char str1[] = "Hello";
    char str2[] = "World";
    char str3[11];

    strcpy(str3, str1);
    strcat(str3, " ");
    strcat(str3, str2);
    cout << str3 << endl;

    return 0;
}

使用字符串函数strcpy()strcat()来操作字符串:

  • 字符数组str1存储了字符串"Hello",数组长度为6。
  • 字符数组str2存储了字符串"World",数组长度为6。
  • 字符数组str3声明了一个长度为11的字符数组,用于存储两个字符串的拼接结果。
  • strcpy(str3, str1)将字符串"Hello"复制到str3中。
  • strcat(str3, " ")将一个空格字符追加到str3末尾。
  • strcat(str3, str2)将字符串"World"追加到str3末尾。
  • cout << str3 << endl;输出str3中存储的字符串"Hello World",最后一个字符是空格。

该程序的输出结果为:

Hello World

这是两个字符串的拼接结果,使用空格字符分隔。

2、C++风格字符串操作

#include 
#include 
using namespace std;

int main()
{
    string str1 = "Hello";
    string str2 = "World";
    string str3 = str1 + " " + str2;
    cout << str3 << endl;

    return 0;
}

修改点:

  • 变量类型改变:使用了string类型代替char数组类型。
  • 使用+运算符代替strcpy()strcat()函数来拼接字符串。
  • 输出结果相同:输出结果仍然为"Hello World"。

使用string类处理字符串的好处包括:

  • 不需要手动管理字符数组的长度,更容易使用和维护。
  • 使用+运算符简化了字符串的拼接操作。
  • 提供了更多的字符串处理方法和功能。

3、C++其他字符串操作

获取字符串长度:使用string类的length()方法可以获取字符串的长度

string str = "Hello World";
cout << "Length of str: " << str.length() << endl;   // 输出11

查找子字符串:使用string类的find()方法可以查找子字符串在字符串中的位置

string str = "Hello World";
size_t pos = str.find("World");
if (pos != string::npos) {
    cout << "Found 'World' at position: " << pos << endl;  // 输出6
} else {
    cout << "Cannot find 'World'" << endl;
}

截取子字符串:使用string类的substr()方法可以从字符串中截取子字符串

string str = "Hello World";
string subStr = str.substr(6, 5);  // 从字符串第7个字符开始,截取5个字符
cout << "Substring: " << subStr << endl;   // 输出"World"

替换子字符串:使用string类的replace()方法可以替换字符串中的子字符串

string str = "Hello World";
str.replace(6, 5, "how are you");   // 从字符串第7个字符开始,替换5个字符为"how are you"
cout << "New string: " << str << endl;   // 输出"Hello how are you"

4.3.4、string类I/O

cin和运算符>>来将输入存储到string对象中,使用cout和运算符<<来显示string对象。

#include 
#include 
using namespace std;

int main()
{
    char str[80];
    cout << "Enter a string: ";
    cin.getline(str, 80);

    for (int i = strlen(str) - 1; i >= 0; i--) {
        cout << str[i];
    }
    cout << endl;

    return 0;
}

该程序要求用户输入一个字符串,然后将这个字符串反转并输出。具体解释如下:

  • char str[80];声明了一个长度为80的字符数组str,用于存储用户输入的字符串。
  • cout << "Enter a string: ";输出提示信息,要求用户输入一个字符串。
  • cin.getline(str, 80);使用getline()函数从标准输入读取字符串,并存储到字符数组str中,最多读取80个字符。
  • for循环遍历字符数组str,从末尾开始依次访问每个字符,并输出到标准输出。
  • cout << endl;输出一个换行符,使得输出的字符串和提示信息分隔开来。

运行该程序,例如输入字符串"Hello World",输出结果为:

dlroW olleH

即输入字符串的反转结果。

4.3.5、其他形式的字符串字面值

原始字符串字面值。C++新增原始(raw)字符串,原始字符串,字符表示的就是自己。比如\n表示的是两个常规字符斜杠和n,"双引号也不再表示字符串,因此需要界定符。原始字符串使用"(和"),并用前缀R来识别原始字符串。

cout << R"(Line1
Line2
Line3)" << endl;

输出结果为:

Line1
Line2
Line3

备注:如果在原始字符串中包含"),那么编译器会认为字符串已经结束。所以可以使用R"+*(表示原始字符串的开头,然后用)+*"标识原始字符串的结尾。


4.4、结构简介

结构(Structure)是一种用户自定义的数据类型,在程序中用于组织和存储相关的变量。结构包含一组不同的变量,这些变量可能是不同的数据类型,例如整型、浮点型、字符型等。在C++中,结构通过struct关键字来定义。结构体反映了面向对象的思维。

结构定义的一般形式为:

struct 结构名 {
    变量类型1 变量名1;
    变量类型2 变量名2;
    ...
};

其中,struct表示定义一个结构体,结构名是结构体的名称,后面紧跟着的是一组变量定义,每个变量都可以有自己的数据类型和名称。

例如,以下代码定义了一个名为Student的结构体,包含了学生的姓名、年龄和成绩:

struct Student {
    string name;
    int age;
    double grade;
};

可以使用这个结构体定义一个学生对象,例如:

Student s;
s.name = "Tom";
s.age = 18;
s.grade = 89.5;

这里可以看到,C++和C语言结构的一个不同点就是C++允许在声明结构变量时忽略关键字struct。

另外,结构体也可以包含函数成员,用于实现一些特定的操作。例如:

struct Person {
    string name;
    int age;
    
    void sayHello() {
        cout << "Hello, my name is " << name << ", and I'm " << age << " years old." << endl;
    }
};

Person p;
p.name = "Alice";
p.age = 20;
p.sayHello();   // 输出"Hello, my name is Alice, and I'm 20 years old."

由于结构体允许存储不同数据类型的变量,因此在实际编程中,它经常被用来定义复杂的数据类型,例如学生、汽车、员工等。

其他结构属性

除了成员变量和成员函数之外,结构体还有其他一些属性和特点,包括:

1、结构体作为函数参数

可以将结构体作为函数的参数传递,实现在函数中对结构体的操作。例如:

void printStudent(Student s) {
    cout << "Name: " << s.name << endl;
    cout << "Age: " << s.age << endl;
    cout << "Grade: " << s.grade << endl;
}

Student s = {"Tom", 18, 89.5};
printStudent(s);

在这个例子中,printStudent()函数的参数是一个Student结构体,函数内部可以访问结构体的成员变量并输出。

2、结构体数组

类似于普通变量和数组,结构体也可以定义为数组形式,可以通过循环等方式对结构体数组中的每个元素进行操作。

Student students[5] = {
    {"Tom", 18, 89.5},
    {"Jack", 19, 92.0},
    {"Alice", 20, 88.0},
    {"Bob", 18, 85.5},
    {"Linda", 19, 90.0}
};

for (int i = 0; i < 5; i++) {
    cout << "Name: " << students[i].name << endl;
    cout << "Age: " << students[i].age << endl;
    cout << "Grade: " << students[i].grade << endl;
}

3、结构体指针

结构体也可以定义为指针形式,使其可以更方便地传递和操作。例如:

Student *p = new Student();   // 使用new申请结构体内存
p->name = "Tom";
p->age = 18;
p->grade = 89.5;

在这个例子中,定义了一个指向Student结构体的指针p,并使用new关键字申请结构体内存。可以使用指针访问结构体成员变量,并进行相应的操作。

结构体具有上述特点和属性,在实际编程中常用于定义复杂的数据类型以及对象之间的关联关系。在后面的面向对象编程部分中,结构体也将作为一种重要的概念进行讲解。

4.4.5、结构数组

结构数组指一个数组中的每个元素都是一个结构体。结构数组可以用来存储一组具有相同数据类型的数据,通常用于处理需要对每个元素进行不同操作的情况。

定义一个名为Student的结构体,包含了学生的姓名、年龄和成绩,然后定义了一个Student类型的数组,用于存储多个学生的信息:

struct Student {
    string name;
    int age;
    double grade;
};

Student students[3] = {
    {"Tom", 18, 89.5},
    {"Jack", 19, 92.0},
    {"Alice", 20, 88.0}
};

在这个例子中,定义了一个长度为3的Student类型的数组,包含了三个学生的信息。

通过下标运算符[]来访问结构数组中的元素,例如:

cout << students[0].name << endl;   // 输出"Tom"
cout << students[1].age << endl;    // 输出"19"
cout << students[2].grade << endl;  // 输出"88"

通过数组下标,我们可以访问结构体中的具体成员变量,实现对结构体数组中每个元素的遍历和操作。

结构数组也可以通过循环来遍历,例如:

for (int i = 0; i < 3; i++) {
    cout << students[i].name << endl;
    cout << students[i].age << endl;
    cout << students[i].grade << endl;
}

在这个例子中,使用for循环遍历了结构数组中的每个元素,并输出了学生的姓名、年龄和成绩。

结构数组在实际编程中经常被用来存储一组相关的数据,例如学生、汽车、员工等信息,并通过遍历和操作实现对这些信息的分析和处理。

4.4.6、结构体中的位字段

位字段(Bit field),结构体中使用位操作来存储信息。位字段将一个整型变量分割成若干个不同长度的位字段,每个位字段用来存储特定的信息。使用位字段可以节省内存空间,提高程序的效率。使用:符号来定义位字段。例如:

struct MyFlags {
    unsigned int a:1;
    unsigned int b:2;
    unsigned int c:3;
};

MyFlags f = {1, 2, 3};

在这个例子中,定义了一个名为MyFlags的结构体,包含了一个1位的a字段,一个2位的b字段和一个3位的c字段。可以使用如下方式来访问和修改这些位字段:

cout << f.a << endl;   // 输出1
cout << f.b << endl;   // 输出2
cout << f.c << endl;   // 输出3

f.a = 0;
f.b = 3;
f.c = 5;

当使用位字段存储变量时,需要注意一些细节:

  • 根据C++标准,位字段只能定义为整型、枚举型或bool型。当使用一个非整型变量定义位字段时,如果它的小数部分不为0,则编译器将报错。
  • 整个位字段的长度不能超过该类型的位数。
  • 位字段的长度必须大于或等于0,且小于或等于所在类型的位数。
  • 不同位字段之间可以使用空域来分隔。

使用位字段的优势在于可以极大地缩小内存使用量,但在实际使用中应该谨慎使用,同时还需要注意不同编译器对位字段的支持程度可能会不同。

4.5、共用体

共用体(Union),允许在相同的内存位置存储不同的数据类型,不同的数据类型只能有一个被激活(即只能有一个被赋值)。

在C++中,使用union关键字定义共用体。共用体的变量在内存中具有相同的地址,共用体的大小等于其中最长的变量的大小。

共用体的一般形式为:

union 共用体名 {
    数据类型1 成员名1;
    数据类型2 成员名2;
    // ...
} 变量名;

其中,union表示定义一个共用体,共用体名是共用体的名称,后面紧跟着的是一组成员定义,每个成员都可以有自己的数据类型和名称。

以下是一个示例:

union MyUnion {
    int a;
    char b[4];
    float c;
} unionVar;

unionVar.a = 97;
cout << unionVar.b << endl;   // 输出 "a"
unionVar.c = 1.23;
cout << unionVar.a << endl;   // 输出一个奇怪的数值

示例中,定义了一个名为MyUnion的共用体,包含了一个int类型的变量a、一个char类型的数组b和一个float类型的变量c。共用体变量unionVar占用4个字节的内存空间(共用体大小为最大变量的大小)。当给a赋值时,b中的值会发生变化,即unionVar.b的值为"a"。但是,当给c赋值时,ab的值都会发生变化,因为它们共用同一块内存,即unionVar.a的值不再是原来的97

共用体可以节省内存空间,实际中需要考虑注意使用的场景,尤其是在多线程环境下使用的共用体。在保证代码简洁和高效的同时,也要注意共用体的使用安全。

4.6、枚举

枚举(Enumeration)是一种定义常量的方式,它将一组相关的常量聚集在一起,并指定它们的名称。在C++中,使用enum关键字定义枚举。

枚举的一般形式为:

enum 枚举名 {
    枚举常量1,
    枚举常量2,
    // ...
} 枚举变量;

其中,enum表示定义一个枚举类型,枚举名是枚举的名称,后面紧跟着的是一组枚举常量定义,每个枚举常量都有自己的名称。

以下是一个示例:

enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
};

Weekday today = Monday;
cout << today << endl;   // 输出 0
today = Tuesday;
cout << today << endl;   // 输出 1

在这个例子中,定义了一个名为Weekday的枚举类型,包含了一组表示一周中每一天的常量。枚举变量today的默认值是Monday,可以赋值为其他的枚举常量。枚举常量默认从0开始顺序编号,所以输出today的值时,得到的是Monday的编号0或者Tuesday的编号1。

枚举常量也可以显式地指定数值,例如:

enum Fruit {
    Apple = 1,
    Orange,
    Banana
};

cout << Apple << endl;    // 输出 1
cout << Orange << endl;   // 输出 2
cout << Banana << endl;   // 输出 3

在这个例子中,定义了一个名为Fruit的枚举类型,其中Apple的值是1,OrangeBanana的值分别是前一个枚举常量加1。

枚举常量在程序中使用时具有常量的属性,即值不能被修改。在实际编程中,枚举常量可以用来表示一组相关的状态或者选项,更好地提高代码的可读性和易维护性。

4.6.1、设置枚举量的值

在C++的枚举类型中,每个枚举成员默认都会分配一个常量值,这个默认值是当前成员在枚举中的位置,从0开始自动编号,并逐个递增。

从其他语言的枚举类型中可能会更容易理解的是,C++支持为枚举成员显式声明对应值。

enum Direction {
    NORTH = 10,
    EAST = 20,
    SOUTH = 30,
    WEST = 40
};

在这段代码中,枚举类型Direction定义了四个成员,分别是NORTH、EAST、SOUTH和WEST。显式设置这些成员与每个成员对应的值,其中NORTH的值是10,EAST的值是20,SOUTH的值是30,WEST的值是40。如果没有显式地提供一个值,则该值将自动为前面的值加上一。

当将枚举量分配给变量时,可以将这些变量当作整数类型来使用,因此可以执行简单操作,例如将它们添加在一起、将它们与其他类型的值比较等。

Direction current = EAST;
int num = current; // 将枚举值赋值给整型变量
if (current == EAST) {
    // 执行某些代码
}

在这段代码中,将EAST枚举值赋给Direction类型的变量current,然后将current变量赋给整型变量num。在if条件语句中检查current变量是否等于EAST枚举值。

4.6.2、枚举的取值范围

枚举类型的取值范围与底层类型相关,通常为int类型或unsigned int类型。因此,枚举类型的取值范围与int或unsigned int类型的取值范围相同,即为整型范围。

在C++11标准中,可以使用enum class关键字定义枚举类(Enum class),它们是一种强类型枚举类型,不与整数类型隐式交换,不能进行隐式转换(???)。枚举类类型名可以与其他类型重复,还可以指定底层类型。


备注:这句话的意思是,枚举类类型是一种强类型枚举类型,不可以与整数类型隐式地相互转换。在使用枚举类类型时,需要显式地进行类型转换。传统的枚举类型的底层类型是整数类型,默认情况下,枚举常量是整数常量,可以与整数进行比较和计算,这样可能会导致错误。例如:

enum Color {
    RED,
    BLUE,
    GREEN
};

int color = RED;
if (color == 1) {
    // ...
}

在这个例子中,枚举类型Color的底层类型是int,红、蓝、绿三个常量分别对应0、1、2。在这个if语句中,将枚举类型的RED值赋值给int变量color,然后与整数1进行比较。这样虽然在语法上没有错误,但会使代码更加难以维护和调试。

而枚举类类型则避免了这种问题,枚举类类型为枚举常量提供了局部作用域,不会与其他作用域的常量发生干扰。枚举常量不能与整数类型隐式交换,必须经过显式的类型转换,这样就增强了程序的类型安全性。

enum class Color {
    RED,
    BLUE,
    GREEN
};

Color color = Color::RED;
if (color == Color::BLUE) {
    // ...
}

在这个例子中,为了比较color枚举变量与BLUE枚举常量,需要使用这种形式的if语句,并进行显式类型转换:if (static_cast(color) == static_cast(Color::BLUE))


枚举类的一般形式为:

enum class 枚举类名 : 底层类型 {
    枚举常量1,
    枚举常量2,
    // ...
} 枚举变量;

其中,enum class表示定义一个枚举类类型,枚举类名是枚举的名称,底层类型是可选的,是枚举的底层类型,后面紧跟着的是一组枚举常量定义,每个枚举常量都有自己的名称。

以下是一个示例:

enum class Weekday : int {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
};

Weekday today = Weekday::Monday;
cout << static_cast(today) << endl;   // 输出 0
today = Weekday::Tuesday;
cout << static_cast(today) << endl;   // 输出 1

在这个例子中,定义了一个名为Weekday的枚举类类型,其底层类型为int,包含了一组表示一周中每一天的常量。枚举变量today的默认值是Monday,可以赋值为其他的枚举常量。枚举常量使用作用域限定符(::)来访问。对于Weekday::Monday,输出的值为0;对于Weekday::Tuesday,输出的值为1。

枚举类类型是一种更安全和更强类型化的枚举类型,它能提高程序的安全性和稳定性。

4.7、指针和自由空间

指针存储另一个变量的地址的变量,用指针来传递地址而不是传递值,这在某些情况下是非常有用的,例如,处理大型数据结构的时候,通过传递地址可以避免复制整个结构。

指针可以指向任何类型的变量,包括其他指针,它们可以用于实现高级数据结构,例如链表和树等。在C++中,使用*&运算符在变量名前面和后面来声明和使用指针。

一个简单的示例:

int x = 10;
int *p = &x;          // 声明一个指向整数的指针
cout << *p << endl;   // 输出 10

这个例子中定义了一个整数变量x和一个指向整数的指针p,通过&运算符获取x的地址并将其赋值给p。通过*p可以访问指针p所指向的变量,输出的结果为10

自由空间指程序运行时可用的内存空间,通过newdelete操作符来动态分配和释放内存。动态内存分配非常有用,例如当需要动态构建长度不定的数组或动态内存分配时,或者需要非常大的内存块时。

以下是一个简单的示例:

int *array = new int[10];   // 动态分配10个整型内存
array[0] = 1;
array[1] = 2;
// ...
delete [] array;           // 释放动态分配的内存

使用new运算符动态分配了一个包含10个整型的数组,并返回数组的首地址。可以像使用静态分配的数组一样使用动态分配的数组,例如将值赋给数组的元素,在这个例子中,将第一个元素赋值为1,第二个元素赋值为2。使用delete运算符释放动态分配的内存。注意,在释放数组时,应该使用delete []而不是delete来释放整个数组。

4.7.1、声明和初始化指针

要声明一个指针,需要使用*符号,后面是指针变量的名称,如下所示:

int *p;

这里声明了一个名为p的指针,指向一个整型值。

指针声明后,需要初始化为一个已知的地址或者置空:

int *p = nullptr;  // 或者 int *p = 0;

这将初始化指针p为空指针。

还可以将指针初始化为一个已知变量的地址:

int x = 10;
int *p = &x;

这将指针p初始化为指向变量x的地址。

需要注意的是,使用指针之前一定要确保其被初始化为一个已知的地址,否则程序可能会崩溃或者出现意想不到的结果。

4.7.2、指针的危险

常见的指针问题:

1、未初始化指针:如果使用未初始化的指针,它将指向一个不确定的地址,访问该地址的值可能会导致程序中止或奇怪的行为。应该始终将指针初始化为一个确定的值,例如使用空指针nullptr

2、空指针解引用:如果尝试使用空指针解引用,将会引发段错误并导致程序崩溃。因此,在使用指针之前必须确保它不是一个空指针。

3、悬挂指针:悬挂指针是指在程序中已经不再存在的内存地址上存储的指针。在尝试访问这些指针指向的数据时,将会导致程序崩溃或不可预测的行为。避免悬挂指针的一种常见方法是及时释放已动态分配的内存。

4、野指针:野指针是指指向未初始化的、已删除的或永久失效的内存地址的指针。使用野指针会导致程序崩溃或不可预测的行为。指针不需要的时候需要指向NULL或者释放内存。

5、坏地址访问:访问不存在的地址,比如数组之外的元素,动态分配的内存边界。

使用指针需要特别注意指针的使用方式生存周期

4.7.3、指针和数字

指针和数字的区别是:指针是地址,数字是数值。

指针用于指向内存中的数据,而数字则用于数值计算和其他算术操作。

1、+-使指针向前或向后移动n个元素:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;      // 指向数组开头
p = p + 3;         // 指向数组中的第4个元素

p指向数组arr的第一个元素,将指针p移动3个元素,p现在指向arr[3]

2、将指针和数字进行比较,比较的是指针所指向的地址与数字的大小关系。

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;      // 指向数组开头
if (p < arr + 5)   // 指针与数组的末尾比较
{
    // ...
}

这个例子中,p指向数组arr的第一个元素,如果指针p小于数组arr的末尾,if语句将执行。

需要注意指针加上或减去数字会导致指针向地址加上相应的大小,结果指向一个新的地址,而数字加减则只是数值计算。指针和数字进行运算时应该非常谨慎,并严格遵循指针算术规则,避免程序出现不可预测的行为。

4.7.4、使用new来分配内存

C++中,使用new在运行动态分配内存存储数据或对象。new操作符返回一个指向新分配内存的指针,该内存包括预定数量的连续内存块。这提供了一种动态为对象或多个对象分配内存的方法,可以根据代码的需求从堆中取出和释放内存。

使用new一定要按照需求释放内存。delete用来释放new动态分配的内存。没有这一步操作会导致内存泄漏。

使用新分配内存的示例:

int *ptr = new int;
// Use the new memory
*ptr = 10;
// Acess new memory again
cout << *ptr << endl;
// Free the memory
delete ptr;

在这个例子中,使用new分配了一个整数的内存空间,指向新内存的地址指定为ptr。通过’*ptr’来使用新内存,可以将一个整数值赋给新内存,并将其输出到控制台。最后,使用delete对内存进行释放。

需要注意的是,当使用new时,应该始终检查分配是否成功。如果内存不足,new操作将失败,并返回nullptr。如果没有检查,程序将无法确定变量或对象的分配是否正确,且可能会导致程序异常或崩溃。

4.7.5、使用delete来释放内存

在C++中,使用delete关键字释放使用new动态分配的内存,避免内存泄漏并帮助程序维护更好的性能。

在使用delete时应该注意以下几点:

1、delete之前要先释放指向动态分配内存的指针,否则无法删除指针引用的内存块,导致泄漏。

int *p = new int; // 申请内存
// 应用内存
delete p;        // 删除与之关联的内存
p = nullptr;     // 重置指针

这个例子演示了如何使用newint变量分配内存,并使用delete释放内存。当指向内存的指针不再使用时,应该将其设置为nullptr以防止悬挂指针。

2、最好释放动态分配的内存块时不要使用指针的副本

int *p = new int;
int *p2 = p;        // p2指向与p相同的内存
// 应用内存
delete p;          // 释放动态分配的内存块
// 但是p2仍然指向该内存块
p = nullptr;  
cout << *p2 << endl; // 危险!悬挂指针

在这个例子中,p指向动态分配的内存块,p2指向p指向的相同的内存。但是,当使用delete释放p指向的内存时,p2将成为一个悬挂指针,它指向已经被释放的内存。

3、当使用new分配数组时,应该使用delete[]来释放内存。

int *arr = new int[10]; // 申请一个int数组
// 应用数组
delete[] arr;          // 释放数组内存
arr = nullptr;         // 重置指针

在这个例子中,使用new动态分配了大小为10的整数数组内存。为了释放完整的数组,应该使用delete[],而不是仅使用delete来释放单个元素

删除内存的常见错误包括:

1)释放指向内存块中的部分内存的指针。

int *p = new int[10];
// 应用数组的一部分
delete p; // 危险!只释放数组的第一个元素

在这个例子中,只有第一个整数被释放了,而其他九个整数仍然存在内存中。

2)释放或多次释放相同的内存块。

int *p = new int;
// 应用内存
delete p;
delete p; // 危险!重复释放同一内存块

在这个例子中,当尝试两次释放同一内存块时,会导致程序异常或出现其他不良行为

总之,在使用delete关键字时不能忽略内存检查,否则可能会导致内存泄漏或程序崩溃。

4.7.6、使用new来创建动态数组

new可以在运行时动态地创建数组。与静态数组不同,动态数组的长度可以在运行时确定,并且可以释放多余的内存以提高应用程序的性能。

使用new动态创建数组的一般格式是:

type *ptr = new type[length];

其中,type是数组中元素的类型,ptr是指向数组开头的指针,length是数组中元素的数量。new会返回一个指向数组开头的地址,可以将它分配给指针。访问数组的元素可以使用指针或下标运算符。

创建int类型的动态数组示例:

int length = 5;
int *arr = new int[length];
// 应用数组
for (int i = 0; i < length; i++)
{
    arr[i] = i;
    cout << arr[i] << endl;
}
// 释放内存
delete[] arr;
arr = nullptr;

在这个例子中,使用new动态创建一个长度为5的整数数组,分配给指针arr。使用循环为每个数组元素分配一个值,并将其输出到控制台。最后,使用delete[]删除数组,避免内存泄漏,并将指针设置为null。

需要注意的是,在使用new创建动态数组时,不希望数组长度小于零。如果使用负数为数组长度,则new将分配一个非常大的内存块,可能会耗尽计算机资源。

总之,动态数组提供了一种灵活的方法来管理内存和数据,并且能够更好地适应程序的需求。在使用new运算符创建和使用动态数组时,必须始终谨慎且遵守内存管理规则,以避免内存泄漏或其他不良影响。

4.8、指针、数组和指针算术

指针与数组在C++中是密切相关的,可以通过指针访问数组元素,反之亦然。指针算术也非常重要,可以简化数组遍历和指针操作。

指针和数组之间的关系

数组是一组相同类型的元素,存储在连续的内存单元中,每个元素有一个唯一的索引。指针是一个变量,它包含另一个变量或对象的内存地址。在C++中,可以将指针用于数组的元素,以访问和修改这些元素。

如下代码中,使用了指针和数组:

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;           // 将p指向数组的开头
cout << *p << endl;     // 输出数组的第一个元素
p++;                    // 指针移动1,指向数组的第2个元素
cout << *p << endl;     // 输出数组的第二个元素

声明一个大小为5且包含五个整数的数组,将指针p初始化为数组的第一个元素的地址,p指向了数组的第一个元素1。通过该指针解引用可以访问和修改数组元素,接着进行了指针运算,将指针移动到数组的第二个元素,并输出该元素的值2

指针算术

指针算术是指使用指针进行简单的算术运算。指针算术的主要操作是加和减运算符,可以使用它们在指针上移动,以便访问数组中的元素。

使用指针算术示例:

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;           // 将p指向数组的开头
int* q = &arr[3];       // 将q指向数组的第4个元素
int n = q - p;          // 计算q和p之间的元素数
cout << n << endl;      // 输出2,q和p之间有2个元素

这里将一个指针初始化为数组的第一个元素的地址,再将另一个指针初始化为数组的第4个元素的地址。q - p运算代表两个指针之间的元素数。指针减法的结果是一个整数,它告诉我们两个指针之间有多少个元素。还可以使用指针加运算,使指针指向数组中的下一个元素

需要注意的是,指针算术非常棘手,因为指针必须指向合法的内存单元。不得将指针移动到数组之外的内存单元,否则可能会导致未定义的行为和错误,如悬浮指针、越界和其他错误。

指针和多维数组

在C++中,多维数组是一种特殊类型的数组,由两个或多个索引进行索引,使用指针和指针算术来访问和操作多维数组。

如下代码中展示了指向二维数组的指针:

int arr[3][4] = {{1, 2, 3, 4},
                {5, 6, 7, 8},
                {9, 10, 11, 12}};
int* p = *arr;           // 将p指向数组的开头
cout << *p << endl;      // 输出数组的第一个元素
p += 5;                  // 移动指针到第二行的第二个元素
cout << *p << endl;      // 输出第二行第二个元素

首先,声明了一个大小为3x4的二维数组。然后,将一个指向数组的第一个元素的指针初始化为数组的开头。通过向指针加上适当的偏移量来访问多维数组中的特定元素。在这个例子中,p指向数组的第一个元素,通过指针算术移动到数组的第二行的第二个元素,并输出该元素的值。

总之,指针和数组是C++程序中最重要的语言构造之一,它们提供了访问内存和数据的灵活方法。在使用指针和数组时,应该牢记指针算术的规则,以确保只访问合法的内存,并避免任何破坏性错误。

4.8.1、程序说明(精读书再进一步完善)

4.8.2、指针小结

指针是一个特殊的变量,它存储内存地址,可以用于访问和操作内存中的数据。指针变量的类型和指向的数据类型必须匹配,以便正确地解引用和进行指针算术运算。

指针可以用于访问数组元素,也可以与指针算术一起使用,以便在数组中移动。指针算术包括加、减和比较运算符,它们允许在指针上进行简单的算术运算。

注意,在进行指针算术运算时,需要非常小心,以确保不越界不引用空指针,避免出现悬挂指针等问题。在不确定指针的合法性时,最好使用指针 NULL 常量或 nullptr 初始化指针。

指针是一项强大的C++语言功能,可以提供更灵活、更高效的方式来访问和操作内存中的数据。然而,指针也可能会引起一些难以调试的问题,所以在使用指针时需要谨慎,避免潜在的错误和问题。

4.8.3、指针和字符串

在C++中,字符串是一组字符的序列,以空字符’\0’结尾。字符串常用的表示方式是以字符数组或指针的形式存储,指向第一个字符的地址。

指针可以用于访问和操作字符串,例如,通过指针访问字符串中的字符,或通过指针算术移动指针,以便在字符串中访问不同的字符。可以将指向字符串的指针传递给函数,并在函数中对字符串进行操作。

以下是一些示例代码,说明了如何使用指针和字符串:

#include 
using namespace std;

int main() {
    char str[] = "Hello World";
    char *ptr = str;

    // 通过指针访问字符串中的字符
    cout << *ptr << endl;

    // 通过指针算术移动指针
    ptr += 6;
    cout << *ptr << endl;

    // 将指针传递给函数并在函数中对字符串进行操作
    void print_string(char *);
    print_string(str);

    return 0;
}

void print_string(char *str) {
    while (*str) {
        cout << *str;
        str++;
    }
}

在上面的代码中,定义了一个字符数组 str,并将指针 ptr 指向字符串的第一个字符。使用指针解引用运算符 ‘*’ 访问字符串中的字符,使用指针算术运算符加号 ‘+’ 来移动指针并访问不同的字符

函数 print_string,函数接受一个指向字符的指针,使用指针在字符串中遍历每个字符,并将其打印到标准输出。

需要注意的是,在使用指针操作字符串时,必须确保字符串以空字符’\0’ 结尾,以便我们可以在指针遍历字符串时停止遍历。否则,指针可能会继续遍历字符串后面的内存,导致不可预测的结果。

总之,指针是C++语言中一个非常强大的功能,可以提高程序的灵活性和性能。在处理字符串时,我们可以使用指针来访问和操作字符串,要注意遵循指针的安全使用原则,以避免潜在的错误和问题。

4.8.4、使用new来创建动态结构

在C++中,可以使用new操作符动态地创建结构,即在运行时分配内存空间。动态创建结构可以让我们在程序运行时动态地分配内存,以便在需要时创建数据结构,而不需要在程序编译时预先知道要分配多少内存。

使用new操作符可以分配一段指定大小的内存,并返回一个指向该内存的指针。使用指针来访问和操作这段内存,以创建我们需要的数据结构。

以下是一个使用new动态创建结构的示例代码,其中创建了一个简单的结构Person:

#include 
#include 
using namespace std;

struct Person {
    string name;
    int age;
};

int main() {

    // 动态创建结构
    Person *p = new Person;
    p->name = "Alice";
    p->age = 25;

    // 访问结构成员并打印输出
    cout << "Name: " << p->name << endl;
    cout << "Age: " << p->age << endl;

    // 释放已分配的内存空间
    delete p;

    return 0;
}

在上面的代码中,使用new操作符位结构Person动态分配了一段内存,并将其地址赋值给指针p。然后使用指针p访问结构成员,并为其赋值。最后,我们使用delete操作符释放已分配的内存空间,以免造成内存泄漏。

需要注意的是,动态分配内存空间后需要及时释放,避免内存泄漏。同时,在动态创建结构时,我们还需要遵循结构成员的访问规则,如使用指针操作符->访问结构成员,以确保访问正确的内存空间。

总之,使用new动态创建结构可以让我们在程序运行时动态地分配内存并创建数据结构,提高程序的灵活性和可扩展性。但同时也要注意释放已分配的内存空间,以避免内存泄漏和其他问题。

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

在C++中,变量的存储方式取决于其作用域和生存期。常见的变量存储方式包括自动存储、静态存储和动态存储。

1、自动存储:自动存储变量在函数内部声明,存储空间会在进入函数时自动分配,离开函数时自动释放。自动存储变量的生存期和作用域都是函数内部。

例如,下面的代码中,变量a和变量b都是自动存储变量:

void func() {
    int a = 10; // 自动存储变量
    if (a < 20) {
        int b = 20; // 自动存储变量
    }
    // b不能在此处访问
}

2、静态存储:静态存储变量在程序运行期间一直存在,不会因为离开作用域而消失。静态存储变量在程序开始执行时被初始化,直到程序结束时才被销毁。静态存储变量可能在函数内部或全局作用域中声明。

例如,下面的代码中,变量count是静态存储变量,在程序运行期间一直存在,每次调用函数时都能保持之前的值:

int func() {
    static int count = 0; // 静态存储变量
    count++;
    return count;
}

3、动态存储:动态存储变量是使用new或malloc等函数在堆上动态分配内存空间得到的变量。它们的生存期取决于程序对它们的使用,需要手动控制变量的创建和销毁。

例如,在下面的代码中,使用new动态分配了一个整数类型变量,返回其指针:

int *p = new int; // 动态存储变量

在使用完动态存储变量后,需要使用delete或free等函数手动释放内存空间,避免内存泄漏

总的来说,不同的变量存储方式适用于不同的需求场景。自动存储和静态存储适用于较小的、在相对短时间内可以被完全使用的变量。而动态存储适用于需要动态分配内存空间的变量,以支持程序的更灵活的设计。同时,使用动态存储变量也需要更谨慎地操作,避免造成内存泄漏和其他安全问题。

4.9、类型组合

C++中的类型组合是指可以使用不同的方式结合现有的类型来创建新的数据类型。C++语言提供了一些支持类型组合的语法和语义,如结构体、联合体、枚举类型等。

1、结构体:结构体是一种将多个不同数据类型的变量组合成一个单个数据类型的方法。结构体可以包含不同的数据类型的成员变量,在结构体内可以使用点号(.)来访问成员变量。可以像使用普通变量一样使用结构体变量。

例如,下面的代码定义了一个名为Person的结构体,包含了姓名和年龄两个成员变量,然后创建了一个Person类型的结构体变量smith,并为它的成员变量赋值:

// 定义Person结构体
struct Person {
    string name;
    int age;
};

// 创建Person结构体变量
Person smith;
smith.name = "John Smith";
smith.age = 30;

2、联合体:联合体是一种特殊的结构体,它将多个不同的数据类型的成员变量存储在同一块内存空间中。联合体的大小等于其最大成员变量的大小。只能同时访问一个成员变量,因为所有成员变量共享同一块内存空间。

例如,下面的代码定义了一个名为Value的联合体,可以存储int和float类型的数据:

union Value {
    int i;
    float f;
};

3、枚举类型:枚举类型是一种将整数值与标识符相对应的数据类型。枚举类型是一组命名的整数常量,可以通过其标识符来访问这些常量。枚举类型可以定义在函数内部或外部,其成员变量可以具有相同的值。

例如,下面的代码定义了一个名为Direction的枚举类型,包含四个方向:

enum Direction {
    North,
    South,
    East,
    West
};

可以使用Direction类型的变量来表示一个具体的方向:

Direction direction = East;

总之,C++的类型组合提供了丰富多样的语法和语义,可以帮助我们在程序设计中使用不同的方式组合现有的数据类型,以创建更加复杂且灵活的数据类型。结构体、联合体和枚举类型是C++中比较常见的类型组合方式,需要根据具体需求选择合适的类型组合方式。

4.10、数组的替代品

在C++中,除了数组之外,还有一些替代方案,可以用来实现类似数组的功能。以下是一些常见的替代方案:

1、向量(Vector):向量是一个动态数组,可以在运行时动态改变其大小。向量提供了与数组相似的访问方式,可以使用下标访问元素,也可以使用迭代器遍历元素。与数组不同的是,向量的大小可以根据需要扩展或缩小。

例如,下面的代码创建了一个名为numbers的向量,并将其初始化为包含1、2和3三个元素:

#include 
using namespace std;

vector numbers{1, 2, 3};

2、列表(List):列表是一个双向链表,可以在任意位置插入或删除元素。列表不支持随机访问,只能通过迭代器遍历元素

例如,下面的代码创建了一个名为words的列表,并将其初始化为包含三个元素:

#include 
using namespace std;

list words{"apple", "banana", "orange"};

3、映射(Map):映射是一种关联数组,将一个键与一个值相关联。映射提供了与数组类似的访问方式,可以使用键来访问对应的值。

例如,下面的代码创建了一个名为scores的映射,并将其初始化为包含两个键值对:

#include 
using namespace std;

map scores{{"Alice", 90.0}, {"Bob", 80.0}};

4、集合(set)

例如,下面的代码创建了一个名为numbers的集合,并将其初始化为包含三个元素:

#include 
using namespace std;

set numbers{1, 2, 3};

总之,C++提供了许多不同的数据结构来代替数组,可以根据具体的需求选择合适的数据结构。这些替代方案具有不同的性能、复杂度和使用方式,需要根据具体场景进行选择。

4.10.1、模板类vector

在C++中,vector是一个非常常用的类模板,用于实现动态数组。它提供了在数组中添加和删除元素的功能,而且还能够自动为数组分配和释放内存,从而避免了手动管理内存的问题

与数组不同,vector可以根据需要动态改变其大小。它可以自动扩展以容纳更多的元素,并在元素被删除时收缩,以节省内存。因此,它是一个非常适合于动态存储数据的数据结构。

使用vector非常简单,只需要包含头文件vector并创建一个vector对象,然后就可以像使用普通数组一样使用它。例如,下面的代码创建一个vector对象,并将其初始化为包含3个元素:

#include 
using namespace std;

vector numbers{1, 2, 3};

可以使用下标运算符[]访问vector中的元素,也可以使用迭代器遍历vector中的元素。例如,下面的代码演示了如何使用下标运算符[]和迭代器访问vector

#include 
#include 
using namespace std;

int main() {
    vector numbers{1, 2, 3};
    
    // 使用下标运算符[]访问vector中的元素
    cout << numbers[0] << endl;
    cout << numbers[1] << endl;
    cout << numbers[2] << endl;
    
    // 使用迭代器遍历vector中的元素
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << endl;
    }
    
    return 0;
}

输出结果为:

1
2
3
1
2
3

除了常规的添加和删除操作,vector还提供了许多其他实用的方法,例如在指定位置插入元素、移除指定元素、获取vector的大小和容量等。

总之,vector是C++中非常有用的一个类模板,用于实现动态数组。它提供了在数组中添加和删除元素的功能,而且可以自动为数组分配和释放内存,从而避免了手动管理内存的问题。

4.10.2、模板类array

在C++11中,新增了一个名为array的标准库类模板,用于实现固定大小的数组,类似于原始的C++数组。与原始的C++数组不同,array强制指定了其大小,并提供了许多有用的方法来管理它。

使用array的方式与使用普通数组非常类似。首先需要包含头文件,然后创建一个array对象。与vector类似,可以使用下标运算符[]访问array中的元素,也可以使用迭代器遍历array中的元素。

下面的代码演示了如何创建一个array对象,并使用下标运算符和迭代器访问其中的元素:

#include 
#include 
using namespace std;

int main() {
    array numbers{1, 2, 3};
    
    // 使用下标运算符[]访问array中的元素
    cout << numbers[0] << endl;
    cout << numbers[1] << endl;
    cout << numbers[2] << endl;
    
    // 使用迭代器遍历array中的元素
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << endl;
    }
    
    return 0;
}

输出结果为:

1
2
3
1
2
3

备注:使用迭代器遍历vector元素的时候,为什么是*it而不是it?

在遍历vector容器中的元素时,需要使用迭代器进行循环依次访问容器中的各个元素。这个迭代器是指向vector容器中的某一个元素的指针。因此,在使用迭代器指向的元素时,需要解引用迭代器才能得到对应元素的值。

具体来说,在循环遍历vector容器中的元素时,迭代器it是指向vector容器中的某一个元素的指针,而不是元素本身。因此,当需要获取it所指向的元素时,需要使用*it来解引用迭代器,获取它所指向的元素的值。而如果直接使用it,将得到指向元素的指针的值,而不是该元素的实际值。

因此,在遍历vector容器中的元素时,经常会看到类似于*it的解引用操作。

array提供了许多有用的方法,例如获取array的大小、交换两个array、填充array等。下面的代码演示了如何使用一些常见的array方法:

#include 
#include 
using namespace std;

int main() {
    array numbers{1, 2, 3};
    
    // 获取array的大小
    cout << "Size: " << numbers.size() << endl;
    
    // 交换两个array
    array otherNumbers{4, 5, 6};
    numbers.swap(otherNumbers);
    cout << "After swap:\n";
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << endl;
    }
    
    // 填充array
    numbers.fill(0);
    cout << "After fill:\n";
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        cout << *it << endl;
    }
    
    return 0;
}

输出结果为:

Size: 3
After swap:
4
5
6
After fill:
0
0
0

总之,array是一个非常有用的标准库类模板,用于实现固定大小的数组。它提供了与原始的C++数组类似的功能,具有许多有用的方法来管理数组。使用array时需要强制指定其大小,并且不能动态改变其大小。

4.10.3、比较数组、vector对象和array对象

在C++中,数组、vector对象和array对象都可以用来存储一组相同类型的元素。它们的实现方式、使用方式和适用场景都有所不同。

1、数组

数组是最基本的数据结构,它是一组具有相同数据类型的元素的集合,在内存中连续存储。数组的大小在编译时就已经确定,无法改变。数组的最大优点是访问元素的速度非常快,因为元素的位置是已知的。

数组的基本定义方式如下:

int numbers[5] = { 1, 2, 3, 4, 5 }; // 定义一个数组,大小为5,元素分别为1,2,3,4,5

使用数组可以通过下标访问其中的元素,如:

cout << numbers[0] << endl; // 输出第一个元素

2、vector对象

vector对象是一个动态数组,使用起来比C++中的原始数组更加方便。vector支持在任意位置插入和删除元素,并且可以自动调整存储空间。vector的大小可以动态增加或减少,因此非常适用于需要动态改变大小的情况。

vector的基本定义方式如下:

#include 
vector numbers = { 1, 2, 3, 4, 5 }; // 定义一个vector,大小为5,元素分别为1,2,3,4,5

使用vector也可以通过下标访问其中的元素,如:

cout << numbers[0] << endl; // 输出第一个元素

3、array对象

array对象是C++11中新增的一个标准库类模板,用于实现固定大小的数组。与原始的C++数组不同,它强制指定了其大小,并提供了许多有用的方法来管理它。

array的基本定义方式如下:

#include 
// 定义一个array,大小为5,元素分别为1,2,3,4,5
array numbers = { 1, 2, 3, 4, 5 }; 

使用array也可以通过下标访问其中的元素,如:

cout << numbers[0] << endl; // 输出第一个元素

下面我们来比较一下数组、vector对象和array对象的一些特点:

特点 数组 vector对象 array对象
存储方式 静态连续存储 动态连续存储 静态连续存储
大小固定
元素类型固定
可以动态改变大小
访问元素速度

综上所述,数组、vector对象和array对象都有各自的优点和缺点。在实际应用中,应根据需要选择合适的数据结构。当需要静态的、大小固定的数组时,应该使用数组或array对象;当需要动态改变大小的数组时,应该使用vector对象。

4.12、复习题

1、如何声明以下变量?
a. actors是一个包含30个char的数组。
b. betsie是一个包含100个short的数组。
c. chuck是一个包含13个float的数组。
d. dipsea是一个包含64个long double的数组。

a. char actors[30];
b. short betsie[100];
c. float chuck[13];
d. long double dipsea[64];

2、第1章复习题1是否使用了数组模板类而非内置数组?

第1章复习题1行中并没有使用数组模板类(array template class),而是使用了内置数组(built-in arrays)。

3、声明一个包含五个整数的数组,并将其初始化为前五个奇正整数。

int myArr[5] = {1, 3, 5, 7, 9};

4、编写一条语句,将问题3中数组的第一个和最后一个元素的和赋给变量even。

int even = myArr[0] + myArr[4];

5、编写一条语句,显示float数组ideas中第二个元素的值。

std::cout << ideas[1];

6、声明一个char类型的数组,并将其初始化为字符串"cheeseburger"。

char food[] = "cheeseburger";

7、声明一个string对象,并将其初始化为字符串"Waldorf Salad"。

std::string food = "Waldorf Salad";

8、设计一个结构体,描述一个fish,包括鱼的种类(kind),重量(weight)(单位为盎司),长度(length)(单位为分数英寸)。

struct fish {
    std::string kind;
    int weight_oz;
    float length_in;
};

9、声明一个上述结构体类型的变量,并进行初始化。

fish myFish = {"salmon", 8, 15.4};

10、使用枚举(enum)定义一个名为Response的类型,该类型可能的值为Yes、No和Maybe。 Yes应该是1,No应该是0,Maybe应该是2。

enum Response {
    No,
    Yes = 1,
    Maybe = 2
};

11、假设ted是一个double变量。声明一个指针,指向ted,并使用指针显示ted的值。

double ted = 10.2;
double *ptr = &ted;
std::cout << *ptr << std::endl;

12、假设treacle是一个包含10个浮点数的数组。声明一个指针,指向treacle的第一个元素,并使用指针显示数组的第一个和最后一个元素。

float treacle[10] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0};
float *ptr = &treacle[0];
std::cout << *ptr << std::endl;
std::cout << *(ptr+9) << std::endl;

13、编写一个代码片段,询问用户输入一个正整数,然后创建一个该大小的动态整型数组。通过new和vector对象分别实现这个过程。

// 通过 new 实现
int size;
std::cout << "Enter the size of the array: " << std::endl;
std::cin >> size;

int* arr = new int[size];

// 通过 vector 实现
std::vector vec;
std::cout << "Enter the size of the vector: " << std::endl;
std::cin >> size;

vec.resize(size);

14、下面的代码是否有效?如果有效,它会输出什么?

cout << (int*) "Home of the jolly bytes";

这是一种非常错误的指针类型转换,它将将字符串的地址转换为一个整型指针,
然后尝试将其打印输出。这很可能会导致不可预测的结果,
因为我们并没有将字符串的内容转换为一个整数。

15、编写一个代码片段,动态分配一个上述结构体类型的变量,然后读取该结构体对象中的kind成员变量的值。

fish* myFish = new fish;
std::cout << "Enter the fish kind:" << std::endl;
std::cin >> myFish->kind;

16、列表4.6说明了在数字输入后跟随基于行的字符串输入导致的问题。将以下语句:

cin.getline(address,80);

替换为以下语句会对程序的运行产生什么影响?

cin >> address;

将cin.getline(address,80)替换为cin >> address将改变程序的行为。cin >> address只会读取空格之前的内容,并将不会读取整个行,因此任何在空格之后的输入都将保留在输入缓冲区中,将影响程序后续输入操作的正确性。因此,不建议这样做,应该使用getline读取整个行。

编程练习:

1. Write a C++ program that requests and displays information as shown in the following example of output:
What is your first name? Betty Sue
What is your last name? Yewe
What letter grade do you deserve? B
What is your age? 22
Name: Yewe, Betty Sue
Grade: C
Age: 22
Note that the program should be able to accept first names that comprise more
than one word.Also note that the program adjusts the grade downward—that is, up
one letter.Assume that the user requests an A, a B, or a C so that you don’t have to
worry about the gap between a D and an F.

你可能感兴趣的:(C++,c++,学习,笔记)