C++之旅(学习笔记)第2章 用户自定义类型

第2章 用户自定义类型

2.1 引言

  • 用基本类型、const修饰符和声明操作符构造出来的类型,称为内置类型。
  • C++抽象机制的目的:令程序员能够设计并实现他们自己的数据类型。
  • 利用C++的抽象机制从其他类型构造出来的类型称为用户自定义类型,即类和枚举。

2.2 结构

构建新类型的第一步通常是:把所需元素组织成一种数据结构。

一个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;	//通过指针访问
}

2.3 类

把类型的接口(所有代码都可使用的部分)与其实现(可访问外部不可访问的数据)分离开来。在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完全一致。

2.4 枚举

enum class Color( red, blue, green);
enum class Traffic_light { green, yellow, red };
Color col = Color::red;
Traffic_light light = Traffic_light::red;
  • 注意:枚举值(例如:red)的作用域在它们的 enum class 内,因此它们可以在不同的 enum class 中重复使用而不会混淆。
  • 例如:Color::red 是Color的red值;与Traffic_light::red完全不同。
  • enum后面的class表示这个枚举类型是强类型,并且具备独立作用域。

不同的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。

2.5 联合

union是一种特殊的struct,它的所有成员都被分配在同一块内存区域中,因此,union实际占用的空间就是它最大的成员所占的空间。

显然,同一时刻,union中只能保存一个成员的值。

  • 例如:考虑实现一个符号表的表项,它保存着一个名字和一个值,这个值要么是Node*,要么是int类型,程序可能如下:
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;
    //...
}
  • 成员p和i从来不会同时被使用,所以空间浪费。使用union可以解决该问题,例如,把两者都定义为union的成员:
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<Node* t,int>可以保存Node*或者int类型的值。

enum class Type {ptr,num};//Type 可以是ptr或者num
struct Entry {
    string name;
    variant<Node* ,int> v;
};
void f(Entry* pe) {
    if(holds_alternative<int>(pe->v))	//*pe是否保存了int类型?
        cout << get<int>(pe->v);		//获取这个int
    //...
}

在很多使用场景中,variant都比union更简单、更安全。

2.6 建议

  1. 当内置类型过于底层时,优先使用定义良好的用户自定义类型;
  2. 将有关联的数据组织为结构(struct或class);
  3. 用class表达接口与实现的区别;
  4. 一个struct就是一个成员默认的public的class;
  5. 优先使用enum class而不是“普通”enum,以避免很多麻烦;
  6. 避免使用“裸”union;将其与类型字段一起封装到一个类中;
  7. 优先使用std::variant,而不是“裸”union;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值