C++:设计一个抽象数据类型

初次接触c++是在15年,还蛮早的,但之后的学习中由于使用c++的地方比较少,所以没怎么使用c++开发过项目。然而随着最近学习Art和LLVM的时候,用到了很多c++知识,逐渐发现了自己基础知识的薄弱。遂决定写一个c++系列的语法和编程入门总结。(主要参考 c++ primer 和 effective c++, effective系列没有涉及细节,详细内容可以看书来获得)

本文不断更新中。

设计class犹如设计type。—- effective c++ 19

定义 #

struct #

struct Screen{
    
    
};

class #

class Screen{
    
    
};

struct和class的区别 #

struct: 第一个访问控制说明符之前的成员全部是public的。

class: 第一个访问控制说明符之前的成员全部是private的。

组成 #

当定义一个类时,如果自己没有申明拷贝构造函数,拷贝赋值运算符和析构函数,编译器会为我们默认生成。如果没有构造函数,编译器也会为我们合成默认构造函数。—- effective c++ 05

我们可以通过=default来表明根据编译器默认合成。

同样地,如果不使用编译器自动生成的拷贝构造函数和拷贝赋值运算符,应该明确拒绝。 —- effective c++ 06

我们可以通过在函数第一次声明时添加=delete来阻止编译器合成拷贝构造函数和拷贝赋值运算符。

类型成员 #

我们可以定义一个类型成员,有两种方式来定义:

  1. typedef;

    class Screen {
    public:
    	typedef std::string::size_type pos;
    private:
    	pos cursor = 0;
    	pos height = 0, width = 0;
    };
    
  2. using;类型别名

    class Screen {
    public:
    	using pos = std::string::size_type;
    }; 
    

注意: 类型成员定义在类开始的地方,因为之后的成员函数或成员变量会使用到.

数据成员 #

成员变量应该被声明是private的。—- effective c++ 22

可变变量(mutable) #

如果我们希望修改类的某个类型成员,即使在const成员函数内。可以通过在变量的申明中加入 mutable 关键字。

一个可变数据成员永远不会是const的,即使它是const对象的成员

class Screen {
public:
    void some_member() const;
private:
	mutable size_t access_str;
};

void  Screen::some_member() const 
{
	++access_ctr;
}

静态变量(static) #

我们可以在成员的申明之前添加关键字“static”使得其与类关联在一起。

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

构造函数和析构函数 #

构造函数定义:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数

构造函数的组成:没有返回值、构造函数名字与类名相同、包含参数列表和函数体。(多个构造函数的参数数量或参数类型上必须有所区别)

一些限制:构造函数不能被申明为const的。(因为常量属性是在对象初始化之后才有的)

析构函数定义:析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他操作;析构函数释放对象的资源,并销毁对象的非static数据成员。

注意:由于析构函数不能接受参数,因此它不能被重载。对于一个给定类,只会有一个析构函数。

隐式销毁一个内置指针类型的成员不会delete它所指向的对象。

什么时候执行析构函数:

  • 变量在离开其作用域时被销毁。
  • 当一个对象被销毁时,其成员被销毁。
  • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。
  • 对于动态分配的对象,当对指向它的指针应用delete运算符被销毁时。
  • 对于临时对象,当创建它的完整表达式结束时被销毁。

注意:绝对不要再构造和析构过程中调用virtual函数。—- effective c++ 09

构造函数分类 #

默认构造函数 #

在用户未定义任何构造函数时,编译器会合成默认构造函数。

当然也可以明确申明使用编译器合成的默认构造函数:

Sales_data() = default;

如果我们不使用默认构造函数的话,要明确指定删除:

Sales_data() = delete;

如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部都被赋予了类内初始值时,这个类才适合于使用合成的默认构造函数。

拷贝构造函数 #

定义:如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数

如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个,与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数

Sales_data(const Sales_data&);

发生的时机

  1. 将一个对象作为实参传递给一个非引用类型的形参;
  2. 从一个返回类型为非引用类型的函数返回一个对象;
  3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员;

拷贝语义

  1. 类的行为像一个值;(如标准库容器和string类的行为)

    即它应该有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然。

  2. 类的行为像一个指针;(如shared_ptr类提供类似指针的行为)

    行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然。

  3. 类的行为既不像值也不像指针;(如 IO 类型 和 unique_ptr 不允许拷贝和赋值。)

实现: 如果我们要实现自己的拷贝构造函数,注意要拷贝它的每一个成员。(如果我们添加了类成员,相应地,我们也要改变拷贝构造函数)。—- effective c++ 12

实现派生类的拷贝构造函数: 我们不仅要赋值派生类的成员,还要复制基类的成员(可以通过调用基类的拷贝构造函数来完成)。—- effective c++ 12

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
	: Customer(rhs), 				// 调用基类的拷贝构造函数
	  priority(rhs.priority)
{
	LogCall("PriorityCustomer copy constructor");
}

移动构造函数 #

在某些情况下,对象拷贝后就立即被销毁了。这这些情况下,移动而非拷贝对象会大幅度提升性能。

为了让我们的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。

移动构造函数: 第一个参数是该类类型的一个右值引用。任何额外的参数都必须有默认实参。

注意:移动构造函数必须确保移后源对象处于这样一个状态—-销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源—-这些资源的所有权已经归属新创建的对象。

移动构造函数不应该抛出异常,且不分配任何新内存。

StrVec::StrVec(StrVec &&s) noexcept
	: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
	s.elements = s.first_free = s.cap = nullptr;        
}

委托构造函数 #

很好理解,就是一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程。

class Sales_data {
public:
	Sales_data(std::string s, unsigned cnt, double price) :
			bookNo(s), units_sold(cnt), revenue(cnt*price) {}
	Sales_data() : Sales_data("", 0, 0) {}
private:
	...
};

构造函数组成 #

explicit属性 #

在要求隐式转换的程序上下文中,我们可以通过将构造函数申明为 explict 加以阻止。

explicit构造函数只能用于直接初始化。

构造函数初始值列表 #

就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。

虚析构函数 #

析构函数添加了virtual属性。所有的基类的析构函数都应该为虚析构函数。—- effective c++ 07

析构函数决定不应该吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。—- effective c++ 08

成员函数 #

公有的成员函数即是接口。好的接口设计应该很容易被正确使用,不容易被误用。—- effective c++ 18

宁可拿 non-member non-friend 函数替换 member 函数,这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。—- effective c++ 23

virtual属性 #

一个某个函数被声明为virtual属性,那么在所有派生类中它都是虚函数。

作用: 实现c++中的多态性,提高代码复用。

多态: 即多种形式。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在

虚函数调用和非虚函数调用的区别:

  1. 非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。(对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致)
  2. 当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

inline属性 #

声明inline函数的两种方式:

  1. 函数的返回类型前面添加关键字inline
  2. 类内定义的函数默认都是inline函数;

注意:内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。

static成员函数 #

与静态数据成员类似,静态成员函数也不与任何对象绑定在一起,它们不包含 this 指针,作为结果,静态成员函数不能申明为const的,而且我们也不能在static函数中使用this指针。

const成员函数 #

形式:在成员函数的参数列表后紧跟const关键字。这里,const的作用是修改隐式this指针的类型。

默认情况下,this的类型是指向类类型非常量版本的常量指针。

一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。

组成 #

返回类型、函数名称、参数、函数体。

返回类型的设计

  1. 必须返回对象时,别妄想返回其reference。—- effective c++ 21
  2. 避免返回handles指向对象内部成分。—- effective c++ 28

参数的设计

​ 1. 默认情况下 c++ 以 by value 方式传递对象至函数。尽量以 pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题。(当然,以上规则不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当)—- effective c++ 20

  1. 若所有的参数都需要类型转换,请为此采用 non-member 函数。—- effective c++ 24

函数体的设计

  1. 只要定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。所以,尽可能延后变量定义式的出现时间—- effective c++ 26
  2. 尽量少做转型动作。—- effective c++ 27

友元 #

类可以允许其它类访问它的非公有成员,方法是令其它类或函数称为它的友元。

class Sales_data {
friend: Sales_data add(const Sales_data&, const Sales_data&);
public:
	...
private:
	...
};

注意:友元的声明仅仅指定了访问的权限,函数的声明需要在类外声明。

如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明:

extern std::ostream& storeOn(std::ostream &, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen {
	friend std::ostream& storeOn(std::ostream &, Screen &);
};

声明位置:一般来说,最好在类的最开始处集中声明友元。

运算符 #

拷贝赋值运算符 #

如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个 合成拷贝赋值运算符

Sales_data&
Sales_data::operator=(const Sales_data &rhs)
{
...
    return *this;
}

一定要返回reference to *this。并且要正确处理自赋值。—- effective c++ 10 11

移动赋值运算符 #

移动赋值运算符执行与析构函数和移动构造函数相同的工作。另外,移动赋值运算符必须正确处理自赋值:

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
	if (this != rhs)
    {
    	free(); 		// 释放已有元素   
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

一定要返回reference to *this。并且要正确处理自赋值。—- effective c++ 10 11

函数调用运算符 #

作用:如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。(因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活)

struct absInt {
	int operator()(int val) const {
		return val < 0? -val : val;
	}
}

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。

this 指针 #

this总是指向该类实例化出来的对象。另外,this还是一个常量指针,我们不允许改变this中保存的地址。(Type*const this;)

this作为非静态成员函数的隐式参数。

total.isbn(); 
== 
Sales_data::isbn(&total);

作用域 #

类的层次结构 #

基类和派生类。

类的性质 #

访问控制属性(封装性) #

继承属性 #

基类和派生类 #

通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class),其他类则直接或间接地从基类继承而来,这些继承得到的类称为派生类(derived class)。

动态绑定: 在 C++ 语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

基类 #

虚函数: 所有的子类要覆盖的函数(子类有特定行为)都申明为virtual函数。—- effective c++

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。

派生类 #

  1. 定义:派生类必须通过使用派生类列表明确指出它是从哪个(或哪些)基类继承而来的。

  2. 派生类列表的形式: 首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有以下三种访问说明符中的一个:public、protected和private。

  3. 派生类中的虚函数

    派生类可以在它覆盖的函数前使用virtual关键字。也可以在形参列表后面添加override关键字来表明覆盖了基类的虚函数。

  4. 派生类对象及派生类向基类的类型转换

    一个派生类对象包含多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。

    派生类到基类的类型转换: 因为在派生类对象中含有与基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。

单继承 #

公有继承(is-a关系) #

public inheritance意味着 is-a 的关系。

即:如果class D(“Derived”) 以 public形式继承 class B(“Base”),便意味着每一个类型为D的对象同时也是一个类型为B的对象。使用于 base classes 身上的每一件事一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。—- effective c++ 32

  1. 继承时,应该避免遮掩继承而来的名称,在public继承下从来没有人希望如此。—- effective c++ 33

  2. 关于接口和实现的继承详解:—- effective c++ 34

    • pure virtual函数只具体指定接口继承。
    • impure virtual函数具体指定接口继承及其默认实现继承。
    • no virtual函数具体指定接口继承以及强制性实现继承。
  3. 绝不重新定义继承而来的默认参数值。

    因为虚函数动态绑定,而默认参数值是静态绑定的。 —- effective c++ 37

私有继承 #

多重继承 #

多态属性 #

总结 #