C++快速入门学习笔记(二)

十一、this指针和类的继承

特殊的指针,this

class Human {
char  name;
Human(char  name);
}
Human::Human(char  name){
	name = name;
}

我们看到,在”name= name”之前,所有的语法都没有任何问题:
Human()构造器有一个名为name的参数
虽然他与Human类里边的属性同名,但却是不相干的两样东西,所以并没有错。
可是,问题是怎样才能让构造器知道哪个是参数,哪个是属性呢?

这时候,就需要用到他了 – this指针。
this -> name= name

现在编译器就懂了,赋值操作符的左边将被解释为当前对象的name属性,右边将被解释为构造器的传入来的name参数。

注意:使用this指针的基本原则是:如果代码不存在二义性隐患,就不必使用this指针!
this指针在一些更加高级的方法里也会用到,但我们暂时不想把事情弄得那么复杂。

类的继承

继承是面对对象编程技术的一个核心概念,它使传统的软件开发模式发生了革命性的变化。
继承机制使得程序员可以创建一个类的堆叠层次结构,每个子类均将继承在它的基类里定义的方法和属性。
那么到底有啥作用?
简单地说,通过继承机制,程序员可以对现有的代码进行进一步的扩展,并应用在新的程序中。
那么我们就需要编写一个Animal类作为Turtle类和Pig类的基类。
基类:是可以派生出其他的类,也称为父类或超类。比如这里的Animal类是基类。
子类:是从基类派生出来的类,比如这里的Turtle类和Pig类是子类。
那么Animal类就拥有了Turtle类和Pig类的共同特征:吃东西、睡觉、流口水。
这里我们把这些动作都抽象化为方法eat(), sleep(), drool();
代表吃东西、睡觉、流口水的eat(), sleep(), drool() 是Animal类里的方法,因为每只动物都会做这些动作。
而swim() 方法在Turtle类里实现,climb() 方法在Pig类里实现。
动物都有嘴巴,而嘴巴是名词不是动作,所以要翻译成类的属性,而不能翻译成类的方法。
我们将mouth转变为Animal类的一个成员变量(属性)

接下来,我们要把对继承关系的描述转化为cpp代码,这需要使用如下所示的语法:
class SubClass : public SuperClass { … }
class Pig : public Animal { … }

#include <iostream>
#include <string>
class Animal
{
public:
	std::string mouth;
	void eat();
	void sleep();
	void drool();
};
class Pig : public Animal
{
public:
	void climb();
};
class Turtle : public Animal
{
public:
	void swim();
};
void Animal::eat()
{
	std::cout << "I'm eatting!" << std::endl;
}
void Animal::sleep()
{
	std::cout << "I'm sleeping!Don't disturb me!" << std::endl;
}
void Animal::drool()
{
	std::cout << "流口水。。" << std::endl;
}
void Pig::climb()
{
	std::cout << "爬树。。" << std::endl;
}
void Turtle::swim()
{
	std::cout << "游。。" << std::endl;
}
int main()
{
	Pig pig;
	Turtle turtle;
	pig.eat();
	turtle.eat();
	pig.climb();
	turtle.swim();
	return 0;
}

十二、继承机制中的构造器和析构器

正如此前所讲解的,cpp 支持程序员自己写出将创建或销毁一个对象时自动调用的方法,也就是构造器和析构器。
在没有继承机制的情况下,我们很容易理解这些方法在创建或销毁一个对象的时候被调用。但是一旦使用了继承机制,构造器和析构器就变得有点复杂了。

比如基类有个构造器,如Animal(),它将在创造 Pig 类型的对象时最先被调用,如果 Pig 类也有一个构造器,它将排在第二个被调用。因为基类必须在子类之前初始化原则!

然后我们继续讨论:如果构造器带着输入参数,事情变得稍微复杂了。

class Animal{
public:
	Animal( std::string  theName );
	std::string  name;
}
class Pig : public Animal{
public:
	Pig( std::string  theName );
}
Animal::Animal( std::string  theName ){
	name = theName;
}
Pig::Pig( std::string  theName ) : Animal( theName ){
}

注意在子类的构造器定义里的”:Animal(theName)”语法含义是:
当调用 Pig() 构造器时(以 theName 作为输入参数),Animal()构造器也将被调用( theName 输入参数将传递给它)。

于是,当我们调用 Pig pig(“小猪猪”); 将把字符串 “小猪猪” 传递给 Pig() 和 Animal(),赋值动作将实际发生在 Animal() 方法里。

在销毁某个对象时,基类的析构器也将被自动调用,但这些事情编译器会自动替你处理。
因为析构器不需要输入参数,所以根本用不着使用 :SuperClassMethod(arguments) 语法!

与构造器的情况相反,基类的析构器将在子类的最后一条语句执行完毕后才被调用。

#include <iostream>
#include <string>
class BaseClass
{
public:
	BaseClass();
	~BaseClass();
	void doSomething();
};
class SubClass : public BaseClass
{
public:
	SubClass();
	~SubClass();
};
BaseClass::BaseClass()
{
	std::cout << "进入基类构造器。。。。。\n";
	std::cout << "我在基类构造器里边干了某些事。。。。\n\n";
}
BaseClass::~BaseClass()
{
	std::cout << "进入基类析构器.......\n";
	std::cout << "我在基类析构器里边也干了某些事。。。。\n\n";
}
void BaseClass::doSomething()
{
	std::cout << "我干了某些事。。。。\n\n";
}
SubClass::SubClass()
{
	std::cout << "进入子类构造器.....\n";
	std::cout << "我在子类构造器里边还干了某些事.....\n\n";
}
SubClass::~SubClass()
{
	std::cout << "进入子类析构器......\n";
}
int main()
{
	SubClass subclass;
	subclass.doSomething();
	std::cout << "完事,收工!\n";
	return 0;
}

在这里插入图片描述

十三、访问控制

关于构造器的设计要越简明越好!我们应该只用它来初始化各种有关的属性。

作为一个基本原则,在设计、定义和使用一个类的时候,应该让它的每个组成部分简单到不能再简单!

最后一点别忘了,析构器的基本用途是对前面所做的事情进行清理。尤其是在使用了动态内存的程序里,析构器将至关重要!

在此前的例子里,我们无论是Animal, Pig 和 Turtle 类的所有成员都是用 public: 语句声明。
所谓访问控制,就是cpp 提供了一种用来保护类里的方法和属性的手段。
这里所说的保护意思是对谁可以调用某个方法和访问某个属性加上一个限制。如果某个对象试图调用一个它无权访问的函数,编译器将报错。

c++ 中的访问级别:
在这里插入图片描述

利用访问级别来保护类里的方法和属性很简单,只要在类里的某个地方写出一个访问级别并在其后加上一个冒号,从那个地方开始往后的所有方法和属性都将受到相应的保护,直到遇到下一个访问级别或者到达这个类的末尾为止!

#include <iostream>
#include <string>
class Animal
{
protected:
    std::string name;
};
class Pig : public Animal
{
};
int main()
{
    Pig pig;
    pig.name = "xxx";//无法直接修改protected类型的name,需要通过set方法,如果是private,那么子类通过set也无法访问
    return 0;
}

使用 private 的好处是,今后可以只修改某个类的内部实现,而不必重新修改整个程序。这是因为其他代码根本就访问不到 private 保护的内容,所以不怕”牵一发而动全身”的惨剧发生!
在同一个类定义里可以使用多个 public:, private: 和 protected: 语句,但最好把你的元素集中到一个地方,这样代码的可读性会好很多。

十四、覆盖方法和重载方法

关于从基类继承来的方法和属性的保护:
class Pig : public Animal { … }
c++ 不仅允许你对在类里定义的方法和属性实施访问控制,还允许你控制子类可以访问基类里的哪些方法和属性。
public是在告诉编译器:继承的方法和属性的访问级别不发生任何改变 – 即 public 仍可以被所有代码访问,protected 只能由基类的子类访问,privat 则只能由基类本身访问。
protected把基类的访问级别改为 protected , 如果原来是 public 的话。这将使得这个子类外部的代码无法通过子类去访问基类中的 public 。
private是在告诉编译器从基类继承来的每一个成员都当成 private 来对待,这意味着只有这个子类可以使用它从基类继承来的元素。
一般都只用 public 而已!

覆盖方法

  1. 通过之前的学习,我们已经知道了如何通过创建新的子类来重用现有的代码(继承)。

  2. 虽然这个方案可以让我们轻松解决许多现实世界里的问题,但在某些场合,却又显得不够用。

  3. 例如当我们需要在基类里提供一个通用的函数,但在它的某个子类里需要修改这个方法的实现,在 c++ 里,覆盖(overriding)就可以做到。

  4. 回到我们之前的例子,我们都知道,但凡是个动物都知道吃!那么吃我们就可以说是动物的一个共同特征,但我们知道不同动物会有不同的吃法。

  5. c++ 可以让我们很容易实现这种既有共同特征又需要在不同的类里有不同实现的方法。

  6. 我们需要做的是在类里重新声明这个方法,然后再改写一下它的实现代码(就像它是一个增加的方法那样)就行啦。

#include <iostream>
#include <string>
class Animal
{
public:
    Animal(std::string theName);
    void eat();
protected:
    std::string name;
};
class Pig : public Animal
{
public:
    Pig(std::string theName);
    void eat();                     // new!
};
Animal::Animal(std::string theName)
{
    name = theName;
}
void Animal::eat()
{
    std::cout << "I'm eatting!" << std::endl;
}
Pig::Pig(std::string theName) : Animal(theName)
{
}
void Pig::eat()
{
    Animal::eat();
    std::cout << name << "正在吃鱼!\n\n";         // new!
}
int main()
{
    Pig pig("小猪猪");
    pig.eat();
    return 0;
}

重载方法

  1. 简化编程工作和提高代码可读性的另一种方法是对方法进行重载。
  2. 重载机制使你可以定义多个同名的方法(函数),只是它们的输入参数必须不同。(因为编译器是依靠不同的输入参数来区分不同的方法)
  3. 重载并不是一个真正的面向对象特性,它只是可以简化编程工作的捷径。
#include <iostream>
#include <string>
class Animal
{
public:
    Animal(std::string theName);
    void eat();
    void eat(int eatCount);
protected:
    std::string name;
};
class Pig : public Animal
{
public:
    Pig(std::string theName); 
};
Animal::Animal(std::string theName)
{
    name = theName;
}
void Animal::eat()
{
    std::cout << "I'm eatting!" << std::endl;
}
void Animal::eat(int eatCount)
{
    std::cout << "我吃了" << eatCount << "碗馄饨!\n\n";
} 
Pig::Pig(std::string theName) : Animal(theName)
{
} 
int main()
{
    Pig pig("小猪猪");
    pig.eat(); 
    pig.eat(15); 
    return 0;
}
  1. 对方法(函数)进行重载一定要有的放矢,重载的方法(函数)越多,程序就越不容易看懂。
  2. 在对方法进行覆盖(注意区分覆盖和重载)时一定要看仔细,因为只要声明的输入参数和返回值与原来的不一致,你编写出来的就将是一个重载方法而不是覆盖方法。这种错误往往很难调试!
  3. 对从基类继承来的方法进行重载,程序永远不会像你预期的那样工作!
#include <iostream>
#include <string>
class Animal
{
public:
    Animal(std::string theName);
    void eat();
protected:
    std::string name;
};
class Pig : public Animal
{
public:
    Pig(std::string theName);
    void eat(int eatCount);
};
Animal::Animal(std::string theName)
{
    name = theName;
}
void Animal::eat()
{
    std::cout << "I'm eatting!" << std::endl;
}
Pig::Pig(std::string theName) : Animal(theName)
{
}
void Pig::eat(int eatCount)
{
    std::cout << "我吃了" << eatCount << "碗馄饨!\n\n";
}
int main()
{
    Pig pig("小猪猪");
    // pig.eat(); //这里会编译报错,因为子类找不到无参的eat方法 
    pig.eat(15); //相当于覆盖了
    return 0;
}

十五、友元关系

  1. 在编程中我们通过 public, protected 和 private 这些访问级别可以让程序员控制谁有权使用某个类里的某个方法和属性。这个强大的方案可以把代码的实现细节掩藏起来,不让没有相应权限的其他代码访问到。

  2. 可是在某些场合,一个完全无关的类由于某些特殊原因需要访问到某个 protected 成员,甚至某个 private 成员,那该怎么办呢?

  3. 有些朋友可能会说,那就把所有的东西都声明成 public 吧。这样固然可以,但这样不就把原来我们想方设法要保护的方法或属性又暴漏了吗?!

  4. 由于这种特殊且尴尬的矛盾存在,cpp 的发明者为此准备了一个解决方案:友元关系。

  5. 友元关系是类之间的一个钟特殊关系,这种关系不仅允许友元类访问对方的 public 方法和属性,还允许友元访问对方的 protected 和 private 方法和属性。
    声明一个友元关系的语法很简单,只要在类声明里的某个地方加上一条 friend class ** 就行了。
    注:这条语句可以放在任何地方,放在 public, protected, private 段落里都可以。

#include <iostream>
#include <string>
class Animal
{
public:
    Animal(std::string theName);
private:
    std::string name;
    friend class Pig; //友元关系,可以访问animal的private name
};
Animal::Animal(std::string theName)
{
    name = theName;
}
class Pig
{
public:
    void change(Animal animal);
};
void Pig::change(Animal animal)
{
    std::cout << "name is :" << animal.name << "!\n\n";//有权限
}
int main()
{
    Animal spiderman("蜘蛛侠");
    Pig pig;
    pig.change(spiderman);
    return 0;
}

十六、静态属性和静态方法

  1. 面对对象编程技术的一个重要特征是用一个对象把数据和对数据处理的方法封装在一起。大家还记得,在前边的例子里我们一直是在使用对象(也可以说某个类的实例)来调用方法,每个方法只处理调用它的那个对象所包含的数据,所有的数据都属于同一个对象。这就引发了一个问题:如果我们所需要的功能或数据不属于某个特性的对象,而是属于整个类的,该怎么办?
  2. 我们不妨假设现在需要统计一下有多少只活的动物。那么我们需要有一个计数器变量:每诞生一只宠物,就给宠物计数器加上1; 没挂掉一只,就建去1。
    我们首先想到的是创建一个全局变量来充当这个计数器,但这么做的后果是程序中的任何代码都可以修改这个计数器,稍不小心就会在程序里留下一个难以查堵的漏洞。
    所以坚决不建议在非必要的时候声明全局变量。
  3. 我们真正需要的是一个只在创建或删除对象时候才允许访问的计数器。
    这个问题必须使用 cpp 的静态属性和静态函数才能完美地得到解决。
    cpp 允许我们把一个或多个成员声明为属于某个类,而不是仅属于该类的对象。
    这么做的好处是程序员可以在没有创建任何对象的情况下调用有关的方法。
  4. 另外一个好处是能够让有关的数据仍在该类的所有对象间共享。
    创建一个静态属性和静态方法:
    只需要在它的声明前加上 static 保留字即可。
#include <iostream>
#include <string>
class Animal
{
public:
    Animal(std::string theName);
    ~Animal();
public:
    std::string name;
    static int count;
    static int getCount();
};
Animal::~Animal()
{
    std::cout << "一个动物被析构" << std::endl;
    count--;
}
int Animal::count=0;//注意这一句:他起码做了两件事 初始化,分配内存
Animal::Animal(std::string theName)
{
    count++;
    name = theName;
    std::cout << "一个动物"<<name <<"被构造" << std::endl;
}
int Animal::getCount()
{
    return count;
}
class Dog : public Animal
{
public:
    Dog(std::string name);
};
Dog::Dog(std::string name):Animal(name){
}
class Pig : public Animal
{
public:
    Pig(std::string name);
};
Pig::Pig(std::string name):Animal(name){
}
int main()
{
    Pig pig("ggboy");
    Dog dog("tom");
    std::cout << "现在有" << Animal::getCount() << "个动物" << std::endl;
    {
        Pig pig("蜘蛛侠");
        Dog dog("tom2");
        std::cout << "现在有" << Animal::getCount() << "个动物" << std::endl;
    } //注意作用范围在这,所以上面两个会调用析构函数
    std::cout << "现在有" << Animal::getCount() << "个动物" << std::endl;
    system("pause");
    return 0;
}

在C/C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区
,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,同时未初始化的对象存储区可以通过void*来访问和操纵,程序结束后有系统自行释放),在cpp里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

静态方法

  1. 静态成员是所有对象共享的,所以不能再静态方法里访问非静态的元素。
  2. 非静态方法可以访问类的静态成员,也可以访问类的非静态成员。

this指针

  1. this指针是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在的对象的地址。

  2. 当一个对象被创建时,该对象的 this指针就自动指向对象数据的首地址。

    #include <iostream>
    #include <string>
    class Dog 
    {
    public:
        Dog(std::string name);
    };
    Dog::Dog(std::string name){
        std::cout << "this:" << this << std::endl;
    }
    int main()
    {
        Dog dog("tom");
        std::cout << "&dog:" << &dog << std::endl;
        system("pause");
        return 0;
    }
    
    // 当对象调用构造函数时,即将dog对象的地址传递给了this指针。
    // 构造函数的原型事实上应该是Dog( Dog *this,std::string name);
    // 第一个参数是指向该类对象的一个指针,我们在定义函数时没看见是因为这个参数在类中是隐含的。
    // 这样 dog 的地址传递给了this,所以在函数中便可以显式的写成  Dog(std::string name) { this->name = name;} 
    

在这里插入图片描述

  1. 在任何一个方法里都可以使用 this 指针。从本质上讲,cpp中的对象其实是一种特殊的结构 – 除了变量,还包含着一些函数的特殊结构。

  2. 在程序运行时,对象的属性(变量)和方法(函数)都是保存在内存里,这就意味着它们各自都有与之相关联的地址。

  3. 这些地址都可以通过指针来访问,而 this指针毋庸置疑是保存着对象本身的地址。

  4. 每当我们调用一个方法的时候,this指针都会随着你提供的输入参数被秘密的传递给那个方法。

  5. 正是因为如此,我们才能在方法里像使用一个局部变量那样使用 this指针。

  6. 因为静态方法不是属于某个特定的对象,而是由全体对象共享的,这就意味着它们无法访问 this指针。所以,我们才无法在静态方法里访问非静态的类成员。

  7. **在使用静态属性的时候,千万不要忘记为它们分配内存。**具体做法很简单,只要在类声明的外部对静态属性做出声明(就像声明一个变量那样)即可。
    静态方法也可以使用一个普通方法的调用语法来调用,但建议不要这么做,那会让代码变得更糟糕!
    请坚持使用:ClassName::methodName();
    请不要使用:objectName.methodName();

十七、虚方法

虚方法(virsual method),因为这是一个很不容易理解的概念,但它在比较抽象的代码里边是不可少的。

引发问题:使用指向对象的指针

我们将使用指针代替局部变量来容纳 Pet 对象。
需要我们认识两个新的cpp保留字:new和delete
前边我们已经讲解过一些关于指针的知识,说白了就是一种专门用来保存内存地址的数据类型。
以前我们常用的做法是:创建一个变量,再把这个变量的地址赋值给一个指针。然后,我们就可以随意地用指针去访问这个变量的值了。

事实上在C和cpp中,我们完全可以在没有创建变量的情况下为有关数据分配内存。也就是直接创建一个指针并让它指向新分配的内存块:

int *pointer = new int;//相当于对malloc的封装
*pointer = 110;
std::cout << *pointer;
delete pointer;

最后一步非常必要和关键,这是因为程序不会自动释放内存,程序中的每一个 new 操作都必须有一个与之对应的 delete 操作

#include <iostream>
#include <string>
class Pet
{
public:
	Pet(std::string theName);
	void play();
protected:
	std::string name;
};
class Cat : public Pet
{
public:
	Cat(std::string theName);
	void play();
};
class Dog : public Pet
{
public:
	Dog(std::string theName);
	void play();
};
Pet::Pet(std::string theName)
{
	name = theName;
}
void Pet::play()
{
	std::cout << name << "正在玩儿!\n";
}
Cat::Cat(std::string theName) : Pet(theName)
{
}
void Cat::play()
{
	Pet::play();
	std::cout << name << "玩毛线球!\n";
}
Dog::Dog(std::string theName) : Pet(theName)
{
}
void Dog::play()
{
	Pet::play();
	std::cout << name << "正在追赶那只该死的猫!\n";
}
int main()
{
	Pet *cat = new Cat("加菲");
	Pet *dog = new Dog("欧迪");
	cat -> play();
	dog -> play();
	delete cat;
	delete dog;
	return 0;
}

在这里插入图片描述

仔细一瞧,程序与我们的预期不符:我们在 Cat 和 Dog 类里对 play() 方法进行了覆盖,但实际上调用的是 Pet::play() 方法而不是那两个覆盖的版本。WHY?

  1. 程序之所以会有这样奇怪的行为,是因为cpp的创始者希望用cpp生成的代码至少和它的老前辈C一样快。
  2. 所以程序在编译的时候,编译器将检查所有的代码,在如何对某个数据进行处理和可以对该类型的数据进行何种处理之间寻找一个最佳点。
  3. 正是这一项编译时的检查影响了刚才的程序结果:cat 和 dog 在编译时都是 Pet 类型指针,编译器就认为两个指针调用的 play() 方法是 Pet::play() 方法,因为这是执行起来最快的解决方案。
  4. 而引发问题的源头就是我们使用了 new 在程序运行的时候才为 dog 和 cat 分配 Dog 类型和 Cat 类型的指针。这些是它们在运行时才分配的类型,和它们在编译时的类型是不一样的!为了让编译器知道它应该根据这两个指针在运行时的类型而有选择地调用正确的方法(Dog::play() 和 Cat::play()),我们必须把这些方法声明为虚方法。

声明一个虚方法的语法非常简单,只要在其原型前边加上 virtual 保留字即可。virtual void play();
另外,虚方法是继承的,一旦在基类里把某个方法声明为虚方法,在子类里就不可能再把它声明为一个非虚方法了。
这对于设计程序来说是一件好事,因为这可以让程序员无需顾虑一个虚方法会在某个子类里编程一个非虚方法。

class Pet
{
public:
	Pet(std::string theName);
	virtual void play(); //只改了这里 加virtual, 像多态实现一样
protected:
	std::string name;
};

在这里插入图片描述

  • 如果拿不准要不要把某个方法声明为虚方法,那么就把它声明为虚方法好了。
  • 在基类里把所有的方法都声明为虚方法会让最终生成的可执行代码的速度变得稍微慢一些,但好处是可以一劳永逸地确保程序的行为符合你的预期!
  • 在实现一个多层次的类继承关系的时候,最顶级的基类应该只有虚方法。
  • 析构器都应该写成虚方法!从编译的角度看,它们只是普通的方法。如果它们不是虚方法,编译器就会根据它们在编译时的类型而调用那个在基类里定义的版本(构造器),那样往往会导致内存泄露!

十八、抽象方法

  1. 抽象方法(abstract method,也可以成为纯虚函数)是面向对象编程技术的另一个核心概念,在设计一个多层次的类继承关系时常会用到。
    把某个方法声明为一个抽象方法等于告诉编译器这个方法必不可少,但我现在(在这个基类里)还不能为它提供一个实现!
    其实在之前我们已经见过一个应该被声明为抽象方法的好例子了,没错,就是 Pet::play() 方法。
  2. 上一个例子中,现实中既不存在什么都玩的宠物,也不存在什么都能玩的游戏。
    每种宠物都有它自己的玩法,而我们的应对措施是输出一条消息说宠物正在玩。
    现在既然知道了抽象方法这个概念,我们就再也用不着编写那些不必要的代码了。
    抽象方法的语法很简单:在声明一个虚方法的基础上,在原型的末尾加上”=0”。(告诉编译器不用浪费时间在这个类里寻找这个方法的实现!)
class Pet
{
public:
	Pet(std::string theName);
	virtual void play() = 0;
protected:
	std::string name;
};

多态性

  • 多态性是面向对象程序设计的重要特征之一。

  • 简单的说,多态性是指用一个名字定义不同的函数,调用同一个名字的函数,却执行不同的操作,从而实现传说中的”一个接口,多种方法”!

  • 多态是如何实现绑定的?
    编译时的多态性:通过重载实现
    运行时的多态性:通过虚函数实现

  • 编译时的多态性特点是运行速度快,运行时的多态性特点是高度灵活和抽象。

  • 重载与覆盖的区别

    重载1.相同的范围(在同一个类中)2.函数名相同3.参数不同4.virtual可有可无

    覆盖1.不同的范围(在基类和派生类中)2.函数名相同3.参数相同4.基类必须有virtual

析构函数解析

前面讲了析构器都是虚方法,其实是要把他定义成虚方法,为什么?

如果我们把类 ClxBase 析构函数前的 virtual 去掉,那输出结果就应该是:
Do something in class ClsDerived!

也就是说类 ClxDerived 的析构函数压根儿没有调用到!

#include <iostream>
class ClxBase
{
public:
    ClxBase()
    {
    };
    virtual ~ClxBase()
    {
    };
    virtual void doSomething() //virtual 很重要
    {
        std::cout << "Do something in class ClxBase!\n";
    }
};
class ClxDerived : public ClxBase
{
public:
    ClxDerived()
    {
    };
    ~ClxDerived()
    {
        std::cout << "Output from the destructor of class ClxDerived!\n";
    };
    void doSomething()
    {
        std::cout << "Do something in class ClxDerived!\n";
    };
};
int main()
{
    ClxBase *pTest = new ClxDerived;
    pTest -> doSomething();
    delete pTest;
    return 0;
}
  • 一般下类的析构函数里面都是释放内存资源,而析构函数不被调用的话旧会造成内存泄露。
  • 所以,析构器都是虚方法是为了当一个基类的指针删除一个派生类的对象时,派生类的析构函数可以被正确调用。
  • 另外,当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里边存放着虚函数指针。为了节省资源,只有当一个类被用来作为基类的时候,我们才把析构函数写成虚函数

十九、运算符重载

  1. 所谓重载,就是重新赋予新的含义。函数重载是对一个已有的函数赋予新的含义,使之实现新功能。
  2. 其实运算符也可以重载,实际上,我们常常在不知不觉之中使用了运算符重载。
  3. 运算符重载的方法是定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算。
  4. 也就是说,运算符重载是通过定义函数实现的。运算符重载实质上是函数的重载。
//重载运算符的函数一般格式如下:
函数类型  operator  运算符名称(形参表列)
{
对运算符的重载处理
}
//例如我们可以重载运算符 + , 如下:
int operator+(int a, int b)
{
return (a – b);
}

举个例子:实现复数加法 (3, 4i)+ (5, -10i)= (8, -6i)

#include <iostream>
class Complex
{
public:
    Complex();
    Complex(double r, double i);
    Complex complex_add(Complex &d);
    void print();
private:
    double real;
    double imag;
};
Complex::Complex()
{
    real = 0;
    imag = 0;
}
Complex::Complex(double r, double i)
{
    real = r;
    imag = i;
}
Complex Complex::complex_add(Complex &d)
{
    Complex c;
    c.real = real + d.real;
    c.imag = imag + d.imag;
    return c;
}
void Complex::print()
{
    std::cout << "(" << real << ", " << imag << "i)\n";
}
int main()
{
    Complex c1(3, 4), c2(5, -10), c3;
    c3 = c1.complex_add(c2); //无重载版
    std::cout << "c1 = ";
    c1.print();
    std::cout << "c2 = ";
    c2.print();
    std::cout << "c1 + c2 = ";
    c3.print();
    return 0;
}

重载版

Complex operator+(Complex a, Complex b)
{
	return a.complex_add(b);
}
int main()
{
    Complex c1(3, 4), c2(5, -10), c3;
    c3 = c1 + c2; //这样就不用每次都写那么长的函数名字了
    std::cout << "c1 = ";
    c1.print();
    std::cout << "c2 = ";
    c2.print();
    std::cout << "c1 + c2 = ";
    c3.print();
    return 0;
}
//其实,我们还可以对运算符重载函数 operator+ 改写得更简练一些:
class Complex
{
public:
	Complex operator+(Complex &c2); //但是要在类中定义此方法
};
Complex Complex::operator+(Complex &c2)
{
	return Complex(real+c2.real, imag+c2.imag);
}

cpp不允许用户自己定义新的运算符,只能对已有的cpp运算符进行重载。
除了以下5个运算符不允许重载外,其他运算符允许重载:

  • .(成员访问运算符)

  • .*(成员指针访问运算符)

  • ::(域运算符)

  • sizeof(尺寸运算符)

  • ? :(条件运算符)

  1. 重载不能改变运算符运算对象(操作数)个数。
  2. 重载不能改变运算符的优先级别。
  3. 重载不能改变运算符的结合性。
  4. 重载运算符的函数不能有默认的参数。
  5. 重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应该有一个是类对象或类对象的引用。(也就是说,参数不能全部都是cpp的标准类型,这样约定是为了防止用户修改用于标准类型结构的运算符性质)。

运算符重载函数作为类友元函数

”+”运算符是双目运算符,为什么刚刚的例子中的重载函数只有一个参数呢?
解答:实际上,运算符重载函数有两个参数,但由于重载函数是 Complex 类中的成员函数,有一个参数是隐含着的,运算符函数是用 this 指针隐式地访问类对象的成员。
return Complex(real+c2.real, imag+c2.imag);
return Complex(this->real+c2.real, this->imag+c2.imag);
return Complex(c1.real+c2.real, c1.imag+c2.imag);

那么例子中的 c1 + c2,编译系统把它解释为:c1.operator+(c2)

即通过对象 c1 调用运算符重载函数,并以表达式中第二个参数(运算符右侧的类对象c2)作为函数实参。

运算符重载函数除了可以作为类的成员函数外,还可以是非成员函数:放在类外,做 Complex 类的友元函数存在:

class Complex
{
public:
    friend Complex operator+(Complex &c, Complex &d);//friend修饰
};
// 注意,这里作为友元函数,不属于Complex,记得别写 :: 咯!
Complex operator+(Complex &c, Complex &d)
{
    return Complex(c.real+d.real, c.imag+d.imag);
}
  1. 为什么把运算符函数作为友元函数呢?

  2. 因为运算符函数要访问 Complex 类对象的成员,如果运算符函数不是 Complex 类的友元函数,而是一个普通的函数,它是没有权力访问 Complex 类的私有成员的

  3. 由于友元的使用会破坏类的封装,因此从原则上说,要尽量将运算符函数作为成员函数。

重载运算符 ”+”, ”-”, ”*”, ”/” 实现有理数的加减乘除运算。
如 1/8 + 7/8 = 1
如 1/8 – 7/8 = -6/8
如 1/8 * 7/8 = 7/64
如 1/8 / 7/8 = 1/7

#include <iostream>
#include <string>
#include <stdlib.h>
class Rational
{
public:
    Rational(int num, int denom);  // num = 分子, denom = 分母
    Rational operator+(Rational rhs); // rhs == right hand side
    Rational operator-(Rational rhs);
    Rational operator*(Rational rhs);
    Rational operator/(Rational rhs);
    void print();
private:
    void normalize(); // 负责对分数的简化处理
    int numerator;    // 分子
    int denominator;  // 分母
};
Rational::Rational(int num, int denom)
{
    numerator = num;
    denominator = denom;
    normalize();
}
// normalize() 对分数进行简化操作包括:
// 1. 只允许分子为负数,如果分母为负数则把负数挪到分子部分,如 1/-2 == -1/2
// 2. 利用欧几里德算法(辗转求余原理)将分数进行简化:2/10 => 1/5
void Rational::normalize()
{
    // 确保分母为正
    if( denominator < 0 )
    {
        numerator = -numerator;
        denominator = -denominator;
    }
    // 欧几里德算法
    int a = abs(numerator);
    int b = abs(denominator); //abs取绝对值这里可以去掉,因为分母不会为负数
    // 求出最大公约数
    while( b > 0 )
    {
        int t = a % b;
        a = b; //16  8  
        b = t; //8   0  b=0时最大公约数是8
	}
    // 分子、分母分别除以最大公约数得到最简化分数
    numerator /= a;
    denominator /= a;
}
// a   c   a*d   c*b   a*d + c*b
// - + - = --- + --- = ---------
// b   d   b*d   b*d =    b*d
Rational Rational::operator+(Rational rhs)
{
    int a = numerator;
    int b = denominator;
    int c = rhs.numerator;
    int d = rhs.denominator;
    int e = a*b + c*d;//因为分母变成一样,所以b,d是相等的
    int f = b*d;
    return Rational(e, f);
}
// a   c   a   -c
// - - - = - + --
// b   d   b   d
Rational Rational::operator-(Rational rhs)
{
    rhs.numerator = -rhs.numerator;
    return operator+(rhs);
}
// a   c   a*c
// - * - = ---
// b   d   b*d
Rational Rational::operator*(Rational rhs)
{
    int a = numerator;
    int b = denominator;
    int c = rhs.numerator;
    int d = rhs.denominator;
    int e = a*c;
    int f = b*d;
    return Rational(e, f);
}
// a   c   a   d
// - / - = - * -
// b   d   b   c
Rational Rational::operator/(Rational rhs)
{
    int t = rhs.numerator;
    rhs.numerator = rhs.denominator;
    rhs.denominator = t;
    return operator*(rhs);
}
void Rational::print()  // 1/8
{
    if( numerator % denominator == 0 )
        std::cout << numerator / denominator;
    else
        std::cout << numerator << "/" << denominator;
}
int main()
{
    Rational f1(2, 16);
    Rational f2(7, 8);
    // 测试有理数加法运算
    Rational res = f1 + f2;
    f1.print();
    std::cout << " + ";
    f2.print();
    std::cout << " = ";
    res.print();
    std::cout << "\n";
    // 测试有理数减法运算
    res = f1 - f2;
    f1.print();
    std::cout << " - ";
    f2.print();
    std::cout << " = ";
    res.print();
    std::cout << "\n";
    // 测试有理数乘法运算
    res = f1 * f2;
    f1.print();
    std::cout << " * ";
    f2.print();
    std::cout << " = ";
    res.print();
    std::cout << "\n";
    // 测试有理数除法运算
    res = f1 / f2;
    f1.print();
    std::cout << " / ";
    f2.print();
    std::cout << " = ";
    res.print();
    std::cout << "\n";
    return 0;
}

重载<<操作符

  1. C 标准库对左移操作符(<<)进行了重载,让它可以把值发送到一个流去。
    上面的例子,iostream 库对新的 Rational 类表示一无所知,所以不能直接用 << 来输出我们的有理数(分数)。

  2. 另外一个原因也比较重要:因为,重载的含义本身就是可以用相同的名字去实现不同的功能:输入参数方面有所差异救不会有问题。
    当然,我们无法在现有的 ostream 类里专门添加一个新的 operator <<()方法。
    所以我们只能定义一个正常的函数在外部重载这个操作符,这与重载方法的语法大同小异,唯一的区别是不再有一个对象可以用来调用 << 重载函数,而不得不通过第一个输入参数向这个重载方法传递对象。
    注意区别前边我们对四则运算符的重载。

  3. 下面是一个 operator <<()函数的原型:
    std::ostream& operator<<( std::ostream& os, Rational f );

  4. 第一个输入参数 os 是将要向它写数据的那个流,它是以“引用传递”方式传递的。

  5. 第二个输入参数是打算写到那个流里的数据值,不同的 operator <<()重载函数就是因为这个输入参数才相互区别的。

  6. 返回类型是 ostream 流的引用。一般来说,在调用 operator <<()重载函数时传递给它的是哪一个流,它返回的就应该是那个流的一个引用。

class Rational
{
private:
    void normalize(); // 负责对分数的简化处理
    int numerator;    // 分子
    int denominator;  // 分母
    friend std::ostream& operator<<(std::ostream& os, Rational f);//这里有friend修改该重载方法,因为此方法是在ostream类库里的,所以无法访问rational中私有方法
};

std::ostream& operator<<(std::ostream& os, Rational f);//声明重载方法
int main()
{
    Rational f1(2, 16);
    Rational f2(7, 8);
    // 测试有理数加法运算
    std::cout << f1 << " + " << f2 << " == " << (f1+f2) << "\n";//如果第二个参数是rational类的话,那么调用的就是重载后的方法,不是的话就是原始的。
    return 0;
}
std::ostream& operator<<(std::ostream& os, Rational f)
{
    os << f.numerator << "/" << f.denominator;
    return os;
}

二十、多继承

  1. 多继承(multiple inheritance)可能是面向对象编程技术中最惹人争议的功能了。
    这个概念乍看起来很简单,但它可能引起一些难以预料的后果。
    因此,JAVA 和 C# 等面向对象编程语言大都只支持多继承的最简单的版本。
  2. 什么时候需要用到多继承?
    只要你遇到的问题无法只用一个”是一个”关系来描述的时候,就是多继承出场的时候。
    举个例子:在学校里有老师和学生,他们都是人(Person),我们可以用”老师是人”和”学生是人”语法来描述这种情况。
    从面相对象编程角度上来看,我么应该创建一个名为 Person 的基类和两个名为 Teacher 和 Student 的子类,后两者是从前者继承来的。
  3. 问题来了:有一部分学生还教课挣钱(助教),该怎么办?酱紫就存在了既是老师又是学生的复杂关系,也就是同时存在着两个”是一个”关系。
    我们需要写一个 TeschingStudent 类让它同时继承 Teacher 类和 Student 类,换句话说,就是需要使用多继承。
    基本语法:
#include <iostream>
#include <string>
class Person
{
public:
    Person(std::string theName);
protected:
    std::string name;
};
class Teacher : public Person
{
public:
    Teacher(std::string theName, std::string theClass);
protected:
    std::string classes;
};
class Student : public Person
{
public:
    Student(std::string theName, std::string theClass);
protected:
    std::string classes;
};
class TeachingStudent : public Student, public Teacher//多个类用逗号隔开
{
public:
    TeachingStudent(std::string theName, std::string classTeaching, std::string classAttending);
    void introduce();
};
Person::Person(std::string theName)
{
    name = theName;
}
Teacher::Teacher(std::string theName, std::string theClass) : Person(theName)
{
    classes = theClass;
}
Student::Student(std::string theName, std::string theClass) : Person(theName)
{
    classes = theClass;
}
TeachingStudent::TeachingStudent(std::string theName,
                                 std::string classTeaching,
                                 std::string classAttending)
                                 : Teacher(theName, classTeaching), Student(theName, classAttending)
{
}
void TeachingStudent::introduce()
{
    std::cout << "大家好,我是" << Student::name << "。我教" << Teacher::classes << ", ";
    std::cout << "同时我在" << Student::classes << "学习。\n\n";
}
int main()
{
    TeachingStudent teachingStudent("丁丁", "cpp入门班", "cpp进阶班");
    teachingStudent.introduce();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值