抽象和类

OOP最重要的特性:

  • 抽象;
  • 封装和数据隐藏;
  • 多态;
  • 继承;
  • 代码的可重用性;

    C++中的类

    通常来说,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。例如: ```cpp // stock00.h —stock class interface // version 00

    ifndef STOCK00_H

    define STOCK00_H

include

class Stock{ private: std::string company; long shares; double share_val; double total_val; void set_tol(){total_val = shares * share_val;} public: void acquire(const std::string & co, long n, double pr); void buy(long num, double price); void sell(long num, double price); void update(double price); void show(); }; // class和struct一样,末尾都需要分号结尾

endif

  1. <a name="CM7xr"></a>
  2. ### 访问控制
  3. 在上面代码中的`private`、`public`都是用于设置访问控制的,它们都是C++新增的。使用类对象的程序,都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。所以说,公共成员函数时程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。
  4. > 其实,这就体现了OOP的封装和数据隐藏特性。除了private、public之外,C++还有第三个访问控制关键字:protected。封装的另一个例子是将类函数定义和类声明放在不同文件中。
  5. 无论是成员变量还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是OOP主要目标之一,**因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分**。<br />**值得一提的是,**`**private**`**修饰可以省略,因为C++默认访问控制即为**`**private**`。(显示使用private可能更加直观)
  6. <a name="N1YRO"></a>
  7. ### 实现类成员函数
  8. 此处为类声明中的原型函数提供代码(Python貌似不能这样分开来)。成员函数定义域常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数,但是它们还有两个特殊的特征:
  9. - 定义成员函数时,使用作用域解析运算符(`::`)来标识函数所属的类;
  10. - 类方法可以访问类的`private`组件;
  11. 例如下面标识了update成员函数定义时的函数头:
  12. ```cpp
  13. void Stock::update(double price)

作用域解析运算符指定了成员函数的归属!(update简写只能在类作用域中使用)
下面展示类方法的实现(只实现了部分代码):

  1. // stock00.cpp --implementing the Stock class
  2. // version 00
  3. #include<iostream>
  4. #include"stock00.h"
  5. // 这些函数都是针对某一家公司的股票而言的,实际问题中通常包含多家的情况。
  6. void Stock::acquire(const std::string& co, long n, double pr){
  7. company = co;
  8. if(n<0){
  9. std::cout << "Number of shares can't be negative"
  10. << company << " shares set to 0.\n";
  11. shares = 0;
  12. }
  13. else{
  14. shares = n;
  15. }
  16. share_val = pr;
  17. set_tol();
  18. }
  19. void Stock::buy(long num, double price){
  20. if(num < 0){
  21. std::cout << "Number of shares purchased can't be negative."
  22. << "Transaction is aborted.\n";
  23. }
  24. else{
  25. shares += num;
  26. share_val = price;
  27. set_tol();
  28. }
  29. }
  30. // TODO

内联函数

事实上,定义位于类声明中的函数都将自动成为内联函数。比如,前面stock00.h里面的set_tol()函数。在之前有讲过,内联函数常常是短小的函数,set_val是符合这样的要求的。当然,也可以在类声明的外部定义成员函数,使之成为内联函数:

  1. class Stock{
  2. private:
  3. //...
  4. void set_tol(); // 定义分离
  5. public:
  6. //...
  7. };
  8. inline void Stock::set_tol(){
  9. total_val = shares * share_val;
  10. }

定义位于类声明中的函数都将自动成为内联函数。所以不要轻易将类的函数定义在类内部! 内联函数的特殊规则要求每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用,最简单的方法:将内联定义放在定义类的头文件中(可以在类里面,也可以在外面)。(内联函数不会出现重复定义的问题)

使用类

事实上,使用类和基本的数据类型差别不是很大(类是用户自定义的类型)。要创建类对象,可以声明类变量,也可以用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。

在教材中说C++提供了一些工具,可用于初始化对象、让cin,cout识别对象,甚至在相似的类对象之间进行自动类型转换?

类的构造和析构函数

之前的类型,比如整型,结构体等都能够进行初始化,但是到目前为止,我们并没有提供类的初始化。并且由于类内部的封装特性,使得很多成员变量无法在类外部访问,从而,利用之前像结构体那样的初始化方法不合适。类初始化需要用到构造函数,它是类在创建过程中自动为类进行初始化的函数

构造函数

类的构造函数和类同名的成员函数。它是没有返回值的,但是不能用void去修饰它。实际上,构造函数没有声明类型。
下面是Stock类的原型:

  1. Stock(const string& co, long n, double pr = 0.0);

下面是构造函数的定义:

  1. // 构造函数定义
  2. Stock::Stock(const string& co, long n, double pr = 0.0){
  3. company = co;
  4. if(n < 0){
  5. std::cerr << "Number of shares can't be negative;"
  6. << company << " shares set to 0.\n";
  7. shares = 0;
  8. }
  9. else{
  10. shares = n;
  11. }
  12. share_val = pr;
  13. set_tol();
  14. }

如果将参数名命名和类成员变量一样的名称,那么就必须用this关键字才能区分罗!

使用构造函数

定义了构造函数就可以调用构造函数初始化类对象了。
第一种方式是显示调用构造函数:

  1. Stock food = Stock("World Cabbage", 250, 1.25);

第二种是隐式调用:

  1. Stock garment("Furry Mason", 50, 2.5);
  2. // 其等价为:Stock garment = Stock("Furry Mason", 50, 2.5);

除此之外,还可以用new来动态分配内存:

  1. Stock *pstock = new Stock("Electronick Games", 18, 19.0);

类对象的成员函数调用,通常是类似:stack1.show(),但是构造函数不能不能这样使用,构造函数是用来创建对象的。

默认构造函数

默认构造函数是在未提供显示初始值时,用来创建对象的构造函数。例如下面的类对象声明:

  1. Stock fluffy_the_cat; // 采用默认构造函数

如果类定义没有声明(定义)构造函数,那么C++将自动提供构造函数。该默认函数不做任何工作:

  1. Stock::Stock(){}

值得注意的是,只有在没有定义任何构造函数的时候,编译器才会提供默认构造函数。为类定义了构造函数之后,程序员就必须自己编写默认构造函数了。也即是说,如果提供了非默认函数,但是程序员没有编写默认函数,则下面的声明将出错

  1. Stock stock1;

这么做的原因可能是想禁止创建未初始化的对象。如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种:一种是给已有构造函数的所有参数提供默认值;另一种是通过函数重载来定义另一个构造函数。但是需要注意的是,默认构造函数只能有一个!(但是,默认函数通常都会设计为给所有成员提供隐式初始值)

  1. // 第一种
  2. Stock(const string& co = "Error", int n = 0, double pr = 0.0);
  3. // 第二种
  4. Stock();

析构函数

在对象过期的时候,程序会自动调用一个特殊的成员函数,即析构函数,其完成清理工作。如果我们在构造函数使用了 new 来分配内存,那么在析构函数我们就需要使用 delete 语句进行内存的释放。由于前面的例子中构造函数没有使用 new 进行内存的分配,所以析构函数实际上没有需要完成的任务,只需要一个隐式析构函数即可。

也就是说,C++中的构造函数和析构函数是可以不编写的,不编写程序将会提供隐式(默认)的函数。

析构函数名称:在类名前面加上 ~,因此 Stock 类的析构函数为 ~Stock,其原型为:

  1. ~Stock();

由于 Stock 析构函数不承担任何重要工作,因此其定义什么也不做:

  1. Stock::~Stock(){
  2. }

如果想要看到析构函数被调用,可以定义如下:

  1. Stock::~Stock(){
  2. std::cout << "Bye!" << std::endl;
  3. }

范围解析运算符:::

析构函数什么时候被调用,由编译器决定。通常不应在代码中显式掉用析构函数。

  • 如果创建的是静态存储类对象,则其析构函数将在程序结束时被调用;
  • 如果创建的是自动存储类对象,其析构函数在程序执行完代码块时自动被调用;
  • 如果是通过 new 进行创建的对象,则其驻留在栈内存或自由存储区中,当使用 delete 来释放内存是,析构函数被调用;
  • 程序可以创建临时对象来完成特定操作,在这种情况下,程序将在结束对该对象的使用时自动调用其析构函数。

    改进Stock类

    由于最初的 Stock 类是没有构造函数和析构函数的,所以在此对其进行改进: ```cpp // stock00.h —stock class interface // version 00

    ifndef STOCK10_H

    define STOCK10_H

include

class Stock{ private: std::string company; long shares; double share_val; double total_val; void set_tol(){total_val = shares * share_val;} public: // two constructors Stock(); Stock(const std::string & co, long n = 0; double pr = 0.0); // noisy destructor ~Stock(); void acquire(const std::string & co, long n, double pr); void buy(long num, double price); void sell(long num, double price); void update(double price); void show(); }; // class和struct一样,末尾都需要分号结尾

endif

  1. <a name="l4R8x"></a>
  2. #### 类实例化分析
  3. ```cpp
  4. Stock stock1("NanoSmrt", 12, 20.0); // #1
  5. Stock stock2 = Stock("Boffo", 2, 2.0); // #2

以上两种方式都可以创建Stock对象。但是对于第二种#2语法,C++编译器容许两种方式来执行之:

  • 第一种方式:和第一种#1语法完全相同;
  • 第二种方式:允许调用构造函数来创建一个临时对象,然后将该临时对象复制到 _stock2 _中,并丢弃临时对象(调用其析构函数)。
    1. stock2 = stock1;
    上面的代码实现对 _stock2_ 的重新赋值。其与结构赋值一样,给类对象赋值时,将一个对象的成员赋值给另一个。所以上述代码中 stock2 原来的内容将被覆盖。

    注意区分什么时候是赋值,什么时候是引用。python中很多对象之间的赋值,例如numpy,其实是引用赋值(速度快,开销小),修改引用原对象也会被修改。(这主要取决于”=”怎么被定义) 要理解C++对象之间的赋值,首先要了解结构体的赋值:struct_s person_1 = {"yyyy", 20, 180}; 显然结构体的赋值就是将列表中的值分别赋给结构体的成员,这显然不可能是地址(指针)赋值或者引用赋值的。对象和类 - 图1 这说明在结构体中“=”定义为结构对象对应成员的赋值。 由此观之,只有数组是非常特别的,其数组名为其内存块首地址

  1. // stock1之前已经被初始化了
  2. stock1 = Stock("Niffty Foods", 10, 50.0);

上述代码,不是初始化,是将新值赋给stock1。其实现方式是:先构造临时对象,然后实现对象成员的复制,最后析构临时对象。

上述类对象的定义都是产生的自动变量,这些变量存在栈中(FILO),在代码块结束之后,对象也被析构。

C++11 列表初始化

  1. Stock stock1 = {"Niffty Foods", 10, 50.0}

类的列表初始化和数组的列表初始化很像。列表中的元素需要和构造函数进行匹配。
下面代码都是合法的:

  1. // Person 类构造函数为 Person(const char* name, const int age)
  2. Person p1("muye", 24);
  3. Person p2 = Person("feng", 23);
  4. Person p3 = new Person("hh", 22);
  5. Perosn p4 = {"moxiao", 21}
  6. // 如果存在默认构造函数 Person()
  7. // 则还可以用下面代码初始化
  8. Person p5 = new Person;
  9. Person p6;

const 成员函数

  1. void show() const;
  2. # 函数定义
  3. void Stack::show() const {
  4. // TODO
  5. }

确保调用函数不会修改对象,将 const 放在函数的后面。

this 指针

this指针指向调用对象,如果方法需要引用整个调用对象,则可以用表示式 *this。和 Python 的 this 指针差不多,就是表示对象本身,可以通过此指针轻松获取对象的成员。

类作用域

类成员不能在外部直接进行访问,需要借助:(.)、(->)或者作用域解析运算符(::)进行访问。

抽象数据类型

在定义类的时候,成员可以只申明而不定义,那么这样的定义非常适合于抽象。