构建新类型的第一步通常是:把所需元素组织成一种数据结构。
一个struct的例子如下:
struct Vector{
double* elem; //指向元素的指针
int sz; //元素的数量
};
//可通过下述方式定义:
Vector v;
然而,它本身并无太大用处,因为v的elem指针并不指向任何实际内容。为了变得有用,需令v指向某些元素。
例如:
void vector_init(Vector& v,int s){ //初始化Vector类型
v.elem = new double[s]; //分配数组空间,包含s个double类型的值
v.sz = s;
}
new操作符从名为自由存储(也叫动态内存或者堆)的区域中分配内存。分配在自由存储中的对象作用域与创建时所处的作用域无关,它会一直存活,直到调用delete操作符销毁它为止。
Vector的一个简单应用如下:
double read_and_sum(int s) { //从cin读入s个整数,然后返回它们的和;假定s为正
Vector v;
vector_init(v,s);
for(int i = 0; i != s; ++i)
cin >> v.elem[i]; //读入元素
double sum = 0;
for(int i = 0; i != s; ++i)
sum += v.elem[i]; //计算元素的和
return sum;
}
标准库类型使用小写名称,所以自定义的类型的名称通常使用首字母大写(例如:Vector和String),以示区别。
不要试图重新发明vector和string这样的标准库组件;直接使用现成的更明智。
访问struct成员有两种方式,通过名字或者引用来访问时用 . (点),通过指针访问时用 -> 。
例如:
void f(Vector v, Vector& rv, Vector* pv) {
int i1 = v.sz; //通过名字访问
int i2 = rv.sz; //通过引用访问
int i3 = pv->sz; //通过指针访问
}
把类型的接口(所有代码都可使用的部分)与其实现(可访问外部不可访问的数据)分离开来。在C++中,实现上述目的的语言机制被称为类。
类的public成员定义了该类的接口,private成员则只能通过接口访问。public和private成员声明允许以任意顺序出现在类声明中,但是按惯例通常将public声明放在private前面,除非需要特别强调private成员的实现。
例如:
class Vector {
public:
Vector(int s) : elem{new double[s]}, sz{s} { } //构造一个Vector
double& operator[] (int i) { return elem[i];} //通过下标访问元素
int size() {return sz;}
private:
double* elem; //指向元素的指针
int sz; //元素的数量
};
//定义一个Vector类型的变量
Vector v(6); //拥有6个元素的Vector
总的来说,Vector对象是一个“句柄”,它包含指向元素的指针(elem)及元素的数量(sz)。
在这里,我们只能通过Vector的接口访问其表示形式(成员elem和sz)。Vector接口由其public成员构成,包括Vector()、operator和size()。
所以之前的read_and_sum()示例可简化为:
double read_and_sum(int s) {
Vector v(s); //创建一个包含s个元素的动态数组
for(int i = 0; i != s; ++i)
cin >> v.elem[i]; //读入元素
double sum = 0;
for(int i = 0; i != s; ++i)
sum += v.elem[i]; //计算元素的和
return sum;
}
与所属类同名的成员函数被称为构造函数,即它是用来构造类的对象的。与普通函数不同,构造函数在初始化类的对象时一定会被调用。因此定义一个构造函数可以消除类变量未初始化造成的问题。
Vector(int s) : elem{new double[s]}, sz{s} { }
该构造函数使用成员初始值列表来初始化Vector的成员:
这条语句的含义是:首先从自由存储分配能容纳s个double类型的元素的空间,用指向这个空间的指针初始化elem,然后将sz初始化为s。
我们常用的两个关键字struct和class没有本质区别,唯一的不同之处在于,struct的成员默认是public的。例如,我们也可以为struct定义构造函数和其他成员函数,这一点与class完全一致。
enum class Color( red, blue, green);
enum class Traffic_light { green, yellow, red };
Color col = Color::red;
Traffic_light light = Traffic_light::red;
不同的enum class 是不同的类型,这有助于防止对常量的误用。比如:不能混用Traffic_light类与Color类的枚举值
Color x1 = red; //错误:哪个red?
Color y2 = Traffic_light::red; //错误:这个red不属于Color类型
Color z3 = Color::red; //可行
auto x4 = Color::red; //可行:Color::red是Color类型
//类似的,也无法隐式地混用Color与整数类型的值
int i = Color::red; //错误:Color::red不是int类型
Color c = 2; //初始化错误:2不是Color类型
//允许显示地指定从int类型进行转换:
Color x = Color{5}; //可行,但烦琐
Color y {6}; //可行
//类似地,可以显示地将enum值转换到其实际存储类型:
int x = int(Color::red);
如果不想显式指定枚举地名称,并且希望枚举值的类型直接是int(而不需要显式类型转换),可以去掉enum class中的class字样,得到一个“普通的”enum。普通的enum中枚举值进入与enum自身同级的作用域,并且可以被隐式转换为整数数值。例如:
enum Color {red, green, blue};
int col = green;
此处col的值为1。默认情况下,枚举值的整数数值从0开始,逐个加1。
union是一种特殊的struct,它的所有成员都被分配在同一块内存区域中,因此,union实际占用的空间就是它最大的成员所占的空间。
显然,同一时刻,union中只能保存一个成员的值。
enum class Type {ptr,num};//Type 可以是ptr或者num
struct Entry {
string name;
Type t;
Node* p; //如果t == Type::ptr,使用p
int i; //如果t == Type::num,使用i
};
void f(Entry* pe) {
if(pe->t == Type::num)
cout << pe->i;
//...
}
union Value {
Node* p;
int i;
};
对于相同的Value对象而言,现在Value::p和Value::i将被放在相同的内存地址。
因为C++语言本身不负责跟踪union实际存储的值的类型,所以需要程序员手动维护如下代码:
enum class Type {ptr,num};//Type 可以是ptr或者num
struct Entry {
string name;
Type t;
Value v; //如果t == Type::ptr,使用v.p;如果t == Type::num,使用v.i
};
void f(Entry* pe) {
if(pe->t == Type::num)
cout << pe->i;
//...
}
时刻维护类型字段(也叫标记,此处为t)与union中实际类型的对应关系并不容易。
也可以使用标准库类型variant,从而消除大多数需要直接使用union的情形。variant保存给定的类型列表集合中的一个值。
例如,variant
enum class Type {ptr,num};//Type 可以是ptr或者num
struct Entry {
string name;
variant v;
};
void f(Entry* pe) {
if(holds_alternative(pe->v)) //*pe是否保存了int类型?
cout << get(pe->v); //获取这个int
//...
}
在很多使用场景中,variant都比union更简单、更安全。