一、枚举
(一)C枚举?真整数!
考虑下面的程序
#include <stdio.h>
#include <stdlib.h>
typedef enum {spring, summer, autumn, winter} Season;
void printSeason(Season season){
switch(season){
case spring:
printf("spring");
break;
case summer:
printf("summer");
break;
case autmn:
printf("autmn");
break;
case winter:
printf("winter");
break;
default:
printf("Not Season");
}
}
int main() {
printSeason(0);
return 0;
}
因为就相当于,所以完全没问题,可是这完全不符合语义,并且,如果仔细看,我写错了一个季节,这样我还不如写数字,另外,如果我想得到枚举的字面字符串,我必须还得像这样打印,这些在C++必须有所改变。
(二)改进C枚举
1、更严格的类型检查
在 C++ 中,枚举类型(enum class)引入了更严格的类型检查机制,与 C 语言的枚举相比,这是一个显著的改进。在 C 语言中,枚举值可以被隐式地转换为整数,这可能导致意外的类型错误。而 C++ 的枚举类则避免了这种情况,它不会自动转换为整数类型,只有通过显式的类型转换才能进行转换。这种严格的类型检查增强了代码的安全性,减少了由于类型不匹配而导致的错误。
比如
#include <iostream>
enum Color { RED, GREEN, BLUE };
void printColor(Color color) {
switch (color) {
case RED:
std::cout << "Red" << std::endl;
break;
case GREEN:
std::cout << "Green" << std::endl;
break;
case BLUE:
std::cout << "Blue" << std::endl;
break;
default:
std::cout << "Unknown color" << std::endl;
break;
}
}
int main() {
printColor(0);
return 0;
}
2、更灵活的枚举类型设定
C++ 中的枚举类型默认为,但是允许设定其它整型类型,这为编程带来了极大的便利性。
enum Season: unsigned char { SPRING = 'S', SUMMER = 'M', AUTUMN = 'A', WINTER = 'W' };
enum Size: unsigned int { SMALL = 1, MEDIUM = 2, LARGE = 3 };
3、增强的作用域控制
C++ 枚举具有明确的作用域规则,这有效地避免了命名冲突。在 C 语言中,枚举值是全局可见的,可能与其他标识符发生冲突。但在 C++ 的枚举中,枚举值的作用域可以被限制在枚举类内部。
正常情况下,可以像C语言一样引用枚举值,也可以通过枚举的作用域
printColor(RED); // C style enum
printColor(Color::BLUE); // C++11 scoped enum
如果想要禁止直接引用枚举值,可以使用枚举类,像是
enum class Color { RED , GREEN, BLUE };
在上面,仅仅定义的枚举类型称之为
(非作用域枚举类型),与之相反的是下面的在enum关键字后面加上了class、也可加struct(与class等价),称之为(作用域枚举类型)。
二、结构体
同样考虑下面的程序
#include <stdio.h>
struct Student {
char * name;
int age;
float gpa;
};
void printStudent(struct Student s) {
printf("Name: %s \n", s.name );
printf("Age: %d \n", s.age);
printf("GPA: %d \n",s.gpa );
}
int main() {
struct Student s1 = { .name = "John",.age = 20,.gpa = 3.5 };
printStudent(s1);
return 0;
}
定义了一个结构体,还设计了一个操作函数,这两者应当是一体的,但是这仅仅是语义上,在代码层面上,这两者并没有太大关系,最多依赖关系 ,我们必须手动处理它们之间的关系,在使用C语言设计数据结构的过程中,这点尤其明显,倘若,结构体本身变了,那么所有关联的配套函数可能都要改变。
另一方面,我们设计不了结构体的默认值,它们只能是单纯的基础类型默认值,倘若,我们想要名字默认张三,这点我们做不到。
其次,关键字很突兀,一不小心就忘写了,必须使用才能不写,但也不一定。
再者,像是数据结构中,我们一般都在堆内存申请空间,我们必须要手动管理对应的堆空间。
这些都显得C语言的结构体有点笨重。
(一)C++结构体是新类型
在 C++ 中,结构体被明确视为一种新的类型。这与 C 语言存在显著差异。在 C 语言中,结构体更多地被看作是一组数据的集合,而在 C++ 里,结构体具有了更独立和明确的类型特征。这意味着在 C++ 中,可以像使用内置类型一样直接定义结构体变量,无需再使用struct关键字。
struct Student {
char name[50];
int age;
float gpa;
};
void printStudent(Student s) {
std::cout << "Name: " << s.name << std::endl;
std::cout << "Age: " << s.age << std::endl;
std::cout << "GPA: " << s.gpa << std::endl;
}
(二)成员函数的添加
C++ 中的结构体可以包含函数(称之为成员函数member function),这大大增强了结构体的功能性。通过在结构体内部定义成员函数,可以将与数据(结构体内,函数外定义的变量,称之为数据成员data member)相关的操作直接与结构体绑定在一起。
#include <iostream>
struct Student {
char name[50];
int age;
float gpa;
void printStudent(Student s) {
std::cout << "Name: " << s.name << std::endl;
std::cout << "Age: " << s.age << std::endl;
std::cout << "GPA: " << s.gpa << std::endl;
}
};
int main() {
Student s1 = { .name = "John",.age = 20,.gpa = 3.5 };
s1.printStudent(s1);
return 0;
}
成员函数能够直接访问结构体的成员变量,使得数据的处理更加紧密和高效。这使得结构体不仅仅是数据的容器,还具备了一定的行为能力,更符合面向对象编程的思想。
(三)隐含指针:this
一般而言,以C语言实现下的数据结构为例,配套的函数都有一个参数是结构体,这点在C语言中很合理,这算是一种手动联系,但是在C++中,既然函数都放进结构体中了,那么还需要手动联系吗?就比如上面的s1.printStudent(s1);就很突兀,所以,C++自动为我们默认提供了这个参数,我们可以通过名为的指针,这个指针指向本身,比如
#include <iostream>
struct Student {
char name[50];
int age;
float gpa;
void printStudent() {
std::cout << "Name: " << this->name << std::endl;
std::cout << "Age: " << this->age << std::endl;
std::cout << "GPA: " << this->gpa << std::endl;
}
};
int main() {
Student s1 = { .name = "John",.age = 20,.gpa = 3.5 };
s1.printStudent();
return 0;
}
(四)为了安全:访问控制
C语言数据结构中一般存在一些结构体,都有一个成员用于记录某些状态,比如栈的top指针,这对于栈的相关操作十分关键,但是外部可以轻易改变。
typedef struct {
int data[MAX_SIZE];
int top;
} Stack;
我所见过多数的C语言栈的数据结构实现,将对top的检验抛之事外,这相当危险
// 判断栈是否为空
int isEmpty(Stack* stack) {
return stack->top == -1;
}
// 判断栈是否已满
int isFull(Stack* stack) {
return stack->top == MAX_SIZE - 1;
}
// 入栈
void push(Stack* stack, int value) {
if (isFull(stack)) {
printf("Stack is full.\n");
return;
}
stack->top++;
stack->data[stack->top] = value;
}
将内存安全依赖于自觉性,希冀一切都是正常,是不合理的。当然这也可以在配套函数中检验,但是在某些情况,你无法检验一切,或者说你无法完全不相信所有,你需要相信某一些而去检验另一些。
为此,C++的结构体提供有访问控制,使用三个类似于C语言标签的访问修饰符
修饰符 | 访问范围 |
---|---|
public | 默认,在程序任何地方,就像是C语言 |
protected | 只允许结构体内部或者子结构体访问 |
private | 只能在结构体中访问 |
struct Student {
private: // private access specifier , can only be accessed within the struct
char name[50];
int age;
float gpa;
public: // public access specifier , can be accessed anywhere in the program
Student() {
name[0] = '\0';
age = 0;
gpa = 0.0;
std::cout << "Default Constructor" << std::endl;
}
~Student() {
std::cout << "Destructor" << std::endl;
}
void printStudent() {
std::cout << "Name: " << this->name << std::endl;
std::cout << "Age: " << this->age << std::endl;
std::cout << "GPA: " << this->gpa << std::endl;
}
protected: // protected access specifier , can be accessed within the class and its derived classes
int id;
};
(三)自主能力:构造与析构函数
构造函数和析构函数是特殊的成员函数,前者用于在创建对象时初始化对象的数据成员,后者用于在对象销毁时释放对象所占用的资源。
简单来说,定义一个结构体变量时,构造函数被自动调用,当结构体变量消亡,诸如生命周期结束,自动调用析构函数,比如
#include <iostream>
struct Student {
private: // private access specifier , can only be accessed within the struct
char name[50];
int age;
float gpa;
public: // public access specifier , can be accessed anywhere in the program
Student() {
name[0] = '\0';
age = 0;
gpa = 0.0;
std::cout << "Default Constructor" << std::endl;
}
~Student() {
std::cout << "Destructor" << std::endl;
}
void printStudent() {
std::cout << "Name: " << this->name << std::endl;
std::cout << "Age: " << this->age << std::endl;
std::cout << "GPA: " << this->gpa << std::endl;
}
protected: // protected access specifier , can be accessed within the class and its derived classes
int id;
};
int main() {
Student s1 ;
s1.printStudent();
return 0;
}
1、初始化问题
无参构造和析构是默认存在的,它们的函数名字固定为前者是结构体名,另一个是~结构体名。
If the constructor is implicitly-declared or explicitly default constructor is not defined as deleted, it is defined (that is, a function body is generated and compiled) by the compiler , and it has the same effect as a user-defined constructor with empty body and empty initializer list.
如果构造函数被隐式声明或显式默认构造函数未定义为已删除,则由编译器定义(即生成并编译一个函数体),它与空主体和空初始化列表的用户定义构造函数具有相同的效果。
如果显式创建了无参构造函数像上面那样,你就不能
因为无参构造就是不使用参数构造,那为什么没有显式声明就行呢?这涉及到一些概念,简单来说,这种初始化叫做聚合初始化 (Aggregate initialization),没有等号也是,属于列表初始化的一种形式,前面提过。聚合初始化用于初始化“聚合”类型,如果是“类”类型,就像是struct,要想成为“聚合”就不能有三种构造函数
no user-provided, inherited, or explicit constructors
用户提供的、继承的或者explicit修饰的构造器
其中用户提供的,指的是
A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration.
如果一个函数是由用户声明的,并且在首次声明时没有被明确地设置为默认或删除,那么它就是用户提供的函数。
非用户提供的,比如
Student() = default; // 设置为默认,和不写一样的效果
//Student() = delete; // 设置为删除,意思是不存在默认构造
这时候,就可以了
另外需要注意的是,这种初始化,不像是C语言的指定器初始化,这种初始化的初始化顺序必须和声明顺序一致
如果不想默认,就自己提供三参的构造函数
这时候默认构造函数不复存在,也可再次声明,构造函数允许重载。
2、成员初始化列表
因为构造函数本意就是用来初始化,所以C++提供了更加高效的初始化方案——成员初始化列表
Student(): name( "jack" ), age( 0 ), gpa( 0.0 ) {};
在构造函数的括号后面操作,,如果没有值,为空,就会进行值初始化
,也就是赋予默认值,如果有值,进行直接初始化
C++的结构体还具体其它高级特性,这里不一一介绍,好戏还在后头……