Loading

C++ 类和对象

类和对象

三大特性:

  • 封装
  • 继承
  • 多态

封装

类的访问权限

权限 描述
public 类内可以访问,类外可以访问
protected 类内可以访问,类外不可访问,后代可以访问
private 类内可以访问,类外不可访问,后代不可访问

struct 和 class 区别

struct 默认权限为 public,class 默认权限为 private.

成员属性私有化

  • 可以控制读写权限
  • 可以对数据有效性进行检验
#include <string>
#include <iostream>

using namespace std;

class Person {
private:
    string name;
public:
    string getName() {
        return name;
    }
    void setName(string name) {
        this->name = name;
    }
};

int main() {
    Person p;
    p.setName("张三");
    cout << "姓名为:" << p.getName() << endl;
    return 0;
}

对象初始化和清理

构造函数和析构函数

类的构造函数会在每次创建类的新对象时执行,析构函数会在每次删除所创建的对象时执行。

若没有提供构造函数和析构函数,编译器会自动提供空的构造函数和析构函数。

#include <iostream>

using namespace std;

class Person {
public:
	Person() {
		cout << "构造函数被调用" << endl;
	}
	~Person() {
		cout << "析构函数被调用" << endl;
	}
};

int main() {
	{
		Person p; 
	}
	system("pause");
	return 0;
}

构造函数

语法:

classname() {
	...
}

注意:

  • 函数名与类名相同
  • 没有返回
  • 可以有参数,可以重载

析构函数

语法

~classname() {
    ...
}
  • 函数名与类名相同,且名称前有 ~ 符号
  • 没有返回
  • 不可以有参数,不可以重载

构造函数分类

按照是否有参数分类

  • 无参构造
  • 有参构造

按照类型分类

  • 普通构造函数
  • 拷贝构造函数

构造函数调用

  • 隐式调用(Person p(10)
  • 显式调用(Person p = Person(10);
  • 隐式转换调用(Person p = 10
#include <iostream>

using namespace std;

class Person {
public:
	Person() {
		this->age = 0;
		cout << "无参构造函数被调用" << endl;
	}

	Person(int age) {
		this->age = age;
		cout << "有参构造函数被调用" << endl;
	}

	Person(const Person& p) {
		this->age = p.age;
		cout << "拷贝构造函数被调用" << endl;
	}

	~Person() {
		cout << "析构函数被调用" << endl;
	}
	int getAge() {
		return this->age;
	}
	void setAge(int age) {
		this->age = age;
	}
private:
	int age;
};

// 构造函数调用的三种方法
void test1() {
	// 隐式调用构造函数
	Person p1;
	Person p2(10);
	Person P3(p2);

}

void test2() {
	// 显式调用构造函数
	Person p1;
	Person p2 = Person(10);
	Person p3 = Person(p2);
}

void test3() {
	// 隐式转换调用构造函数
	Person p4 = 10;
	Person p5 = p4;
}
int main() {
	test1();
	test2();
	test3();
	return 0;
}

拷贝构造函数调用时机

  • 用已创建的对象初始化一个新对象
  • 函数调用时,以值传递方式给函数参数传参
  • 函数以值方式返回局部对象

构造函数调用规则

默认情况下,C++ 编译器会给一个类自动生成

  • 默认构造函数(空,无参)
  • 默认析构函数(空,无参)
  • 默认拷贝构造函数(值拷贝 )

调用规则

  • 若用户定义了有参构造函数,编译器将不生成默认无参构造函数,但会生成默认拷贝构造函数
  • 若用户定义了拷贝构造函数,编译器将不会生成其他构造函数

深拷贝与浅拷贝

深拷贝:在堆内存重新申请空间,进行拷贝操作

浅拷贝:简单赋值拷贝

默认的拷贝构造函数是进行浅拷贝,因此在类构造时,涉及申请内存时,可能会在析构时对同一个内存释放两次,造成错误。

如果构造函数使用了 new,则必须提供使用 delete 的析构函数。

#include <iostream>

using namespace std;

class Person {
public:
	Person() {
		age = 0;
		hight = new int(160);
	}
	Person(int age, int hight) {
		this->age = age;
		this->hight = new int(hight);
	}

	// 将下面的拷贝构造函数注释,程序会崩
	Person(const Person& p) {
		age = p.age;
		hight = new int(*p.hight);
		cout << "拷贝构造函数被调用" << endl;
	}

	~Person() {
		cout << "析构函数被调用" << endl;
		if (hight != NULL) {
			delete hight;
			hight = NULL;
		}
	}
	int age;
	int* hight;
};

void test() {
	Person p1(18, 160);
	cout << "p1 身高:" << *p1.hight << endl;
	Person p2(p1);
	cout << "p2 身高:" << *p2.hight << endl;
}

int main() {
	test();
	return 0;
}

初始化列表

构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:

class Person
{
public:
	Person(string name, int age):m_name(name),m_age(age){}; // 初始化列表
private:
	string m_name;
    int m_age;
};

类对象作为类成员

C++ 中的成员可以是另一个类对象。

当 A 类作为 B 类的成员,构造时,先调用 A 类的构造函数,再调用 B 类的构造函数。析构时,顺序相反。

例如:

class A{}
class B{
    A a;
}

静态成员变量

  • 所有对象共享一个静态成员变量
  • 在编译阶段分配内存
  • 类内声明,类外初始化

静态成员函数

  • 所有对象共享一个函数
  • 静态成员函数只能访问静态成员变量,不能访问非静态成员变量
class Person
{
public:
    static void func(){
        cout << "调用静态成员函数"
    }
}

// 调用
void test(){
    // 通过对象访问
    Person p;
    p.func();
    
    // 通过类名访问
    Person::func();
}

C++ 对象模型和 this 指针

成员变量和成员函数

  • 成员变量和成员函数分开存储
  • 静态成员变量不存储在类中
  • 非静态成员函数不存储在类中(存储在类中的只有非静态成员变量)
  • 空对象占用 1 个字节(为了区分其他对象)

this 指针

this 指针指向调用它的成员函数所属的对象。

用途:

  • 形参和成员变量同名,可以用 this 指针区分 (也可以在成员变量前加 m_
  • 返回对象本身(return *this;

空指针访问成员函数

空指针可以访问成员函数

#include <string>
#include <iostream>

using namespace std;

class Person {
public:
    string name;
    void showPersonName() {
        // 可以添加判断 this 是否为 NULL 的代码增强健壮性
        cout << this->name << endl;
    }
    void sayHi() {
        cout << "Hi" << endl;
    }
};


int main() {
    Person* p_ptr = NULL;
    p_ptr->sayHi(); // 不会报错
    // p_ptr->showPersonName(); // 报错

    return 0;
}

const 修饰成员函数

常函数:

  • 函数末尾有 const 关键字
  • 常函数不可以修改成员属性
  • 成员属性声明时加 mutable 关键字,则在常函数中可以修改

常对象:

  • 声明对象前加 const 的对象
  • 常对象只能调用常函数

示例:

#include <iostream>

using namespace std;

class Person
{
public:
    void setInfo() const {
        m_a = 0; // 报错:无法修改
        m_b = 0; // 可以修改
    };
    void func(){};
    int m_a;
    mutable int m_b;
};

// 测试常函数
void test1(){
    Person p;
    p.setInfo();
};

// 测试常对象
void test2(){
    const Person p;
    p.func(); // 报错: 无法修改
};

int main()
{
    
    return 0;
}

友元

程序中,需要让类外的函数或类访问私有属性,需要用到友元技术,关键字为 friend

友元的的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

示例:

#include <iostream>
#include <string>

using namespace std;

class Building;

class FriendClass2 {
public:
    FriendClass2();
    void visit();
    Building* building;
};

class Building {
    // 使全局函数可以访问私有成员
    friend void friendFunc(Building& building);

    // 使类可以访问私有成员
    friend class FriendClass;

    // 使类成员函数可以访问私有成员
    friend void FriendClass2::visit();

public:
    Building(string sittingroom = "客厅", string bedroom = "卧室"):
        m_sittingroom(sittingroom), m_bedroom(bedroom) {};
    string m_sittingroom;

private:
    string m_bedroom;
};

void friendFunc(Building& building) {
    cout << "全局函数访问:" << building.m_sittingroom << endl;
    cout << "全局函数访问:" << building.m_bedroom << endl;
}

class FriendClass {
public:
    void visit(Building& building) {
        cout << "类访问:" << building.m_sittingroom << endl;
        cout << "类访问:" << building.m_bedroom << endl;
    }
};

FriendClass2::FriendClass2() {
    building = new Building;
}
void FriendClass2::visit() {
    cout << "类成员函数访问:" << building->m_sittingroom << endl;
    cout << "类成员函数访问:" << building->m_bedroom << endl;

}
int main()
{
    Building building;

    // 全局函数做友元
    friendFunc(building);

    // 类做友元
    FriendClass fc;
    fc.visit(building);

    // 类成员函数
    FriendClass2 fc2;
    fc2.visit();
}

运算符重载

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。

一般通过类内成员函数友元函数重载运算符,下面的示例定义了一个 Time 类,并重载了 +-<< 运算符。

重载 +-<< 运算符

示例:

#include <iostream>

using namespace std;

class Time {
public:

    // 构造函数
    Time(int min = 0, int second = 0) :
        m_min(min), m_second(second) {}

    // 重载 << 运算符用于输出
    friend ostream& operator << (ostream& os, const Time& t) {
        os << t.m_min << " m " << t.m_second << " s ";
        return os;
    }

    // 通过成员函数重载 + 运算符
    // 相当于 t1.opeator+(t2)
    Time operator+ (const Time& t) {
        Time temp;
        temp.m_min = this->m_min + t.m_min + (this->m_second + t.m_second) / 60;
        temp.m_second = (this->m_second + t.m_second) % 60;
        return temp;
    }

    // 通过友元函数重载 - 运算符
    // 相当于 opeator-(t1, t2)
    friend Time operator- (Time& t1, const Time t2) {
        // 演示没有实现进退维
        Time t;
        t.m_min = t1.m_min - t2.m_min;
        t.m_second = t1.m_second - t2.m_second;
        return t;
    }
private:
    int m_min;
    int m_second;
};

void testAdd(){
    Time t1(1, 50);
    Time t2(2, 30);
    Time t3 = t1 + t2;

    cout << "t1 is " << t1 << endl;
    cout << "t2 is " << t2 << endl;
    cout << "t1 + t2 is " << t3 << endl;  
}

void testSub() {
    Time t1(1, 10);
    Time t2(2, 30);
    Time t3 = t1 - t2;

    cout << "t1 is " << t1 << endl;
    cout << "t2 is " << t2 << endl;
    cout << "t1 - t2 is " << t3 << endl;
}
int main()
{
    testAdd();
    testSub();
}

重载递增运算符(++

重载 ++ 运算符

  • 编译器通过判断运算符重载函数参数中是否插入关键字 int 来区分前置递增和后置递增
  • Type& operator++() :前置递增
  • Type operator++(int) :后置递增

示例:重载递增运算符

#include <iostream>

using namespace std;

class MyInteger {

public:
    MyInteger() {
        m_num = 0;
    }
    friend ostream& operator<< (ostream& os, MyInteger myint) {
        os << myint.m_num;
        return os;
    }
    // 重载前置 ++ 运算符
    MyInteger& operator++ () {
        m_num++;
        return *this;
    }
    // 重载后置 ++ 运算符 (注意返回不是引用)
    MyInteger operator++ (int) {
        // 记录当前结果
        MyInteger temp = *this;
        // 后自增
        m_num++;
        // 返回记录结果
        return temp;
    }
private:
    int m_num;
};

void testFrontSelfIncrement() {
    MyInteger myint;
    cout << ++myint << endl;
    cout << ++(++myint) << endl;
    cout << myint << endl;
}

void testEndSelfIncrement() {
    MyInteger myint;
    cout << myint++ << endl;
    cout << (myint++)++ << endl;
    cout << myint << endl;
}
int main()
{
    testFrontSelfIncrement();
    testEndSelfIncrement();
}

重载赋值运算符(=

赋值运算符对属性进行值拷贝。使用默认的赋值运算符,若在构造函数中申请了内存,在析构时可能造成堆区内存重复释放,造成程序崩溃。

下面的代码会崩溃,原因是内存重复释放。

#include <iostream>

using namespace std;

class Person {
public:
    int* m_age;

    Person(int age) {
        m_age = new int(age);
    }
    
    ~Person() {
        if (m_age != NULL) {
            delete m_age;
            m_age = NULL;
        }
    }
};

void test() {
    Person p1(18);

    Person p2(20);

    p2 = p1;

    cout << "p1 的年龄为:" << *p1.m_age << endl;
}
int main()
{
    test();
}

解决方法是重载 = 运算符

// 11-运算符重载-赋值运算符重载.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>

using namespace std;

class Person {
public:
    int* m_age;

    Person(int age) {
        m_age = new int(age);
    }
    
    ~Person() {
        if (m_age != NULL) {
            delete m_age;
            m_age = NULL;
        }
    }

    Person& operator= (Person &p) {
        // 首先判断是否有属性在堆区,若有,需释放内存
        if (m_age != NULL) {
            delete m_age;
            m_age = NULL;
        }

        // 进行深拷贝
        m_age = new int(*p.m_age);

        return *this;
    }
};

void test() {
    Person p1(18);
    Person p2(20);
    Person p3(30);

    p3 = p2 = p1;

    cout << "p1 的年龄为:" << *p1.m_age << endl;
    cout << "p2 的年龄为:" << *p2.m_age << endl;
    cout << "p3 的年龄为:" << *p3.m_age << endl;

}
int main()
{
    test();
}

函数调用运算符重载(()

使用方法类似函数调用,因此又称为仿函数。

#include <iostream>
#include <string>

using namespace std;

class MyPrint {
public:
    void operator()(string s) {
        cout << s;
    }
};

int main()
{
    // 使用匿名对象
    MyPrint()("Hello world!");
}

继承

已有一个实现部分功能的类,想要基于已有类新建一个含有新功能(或不同功能)的类,可以继承已有的类,减少代码量。这个已有的类称为基类,新建的类称为派生类

语法

单继承

// 基类
class Base {
};

//派生类
class Drived : public Base {
};

多继承

// 基类
class Base1;
class Base2;

//派生类
class Drived : public Base1, public Base2 {
};

public 是一种继承方式。开发中不建议使用多继承。

继承方式

  • 公共继承
  • 保护继承
  • 私有继承
img image-20210214195630510

继承中的对象模型

父类中所有的非静态成员属性都会被子类继承,私有属性也会被继承,但是被隐藏了。

继承中构造和析构顺序

基类构造 > 派生类构造 > 派生类析构 > 基类析构

继承中同名成员处理方式

派生类访问基类同名成员需要加上基类的作用域 。

同名静态成员处理方式一致。

class A {
public:
    int a;
    func();
}

Class B: public A{
public:
    int a;
    func();
}

void test(){
    B b;
    // 访问自己的 func
    b.func();
    
    // 访问基类的 func
    b.A::func();
}

菱形继承

两个派生类继承与一个基类,又有某个类同时继承这两个类。

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}

这段代码使用虚继承重新实现了菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

多态

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

分类

  • 静态多态:运算符重载、函数重载
  • 动态多态:使用派生类虚函数实现

静态多态和动态多态区别:

  • 静态多态的函数地址在编译阶段确定(早绑定)
  • 动态多态的函数地址在运行阶段确定(晚绑定)

多态的条件

  • 有继承关系
  • 派生类重写基类的虚函数

使用时,符类指针或引用指向子类对象。

示例:

#include <iostream>

using namespace std;

class Animal {
public:
    // 虚函数
    virtual void speak() {
        cout << "动物在说话" << endl;
    }
    void eat() {
        cout << "动物在吃" << endl;
    }

};

class Cat: public Animal {
public:
    void speak() {
        cout << "喵喵喵" << endl;
    }
    void eat() {
        cout << "猫在吃" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void speak() {
        cout << "汪汪汪" << endl;
    }

    void eat() {
        cout << "狗在吃" << endl;
    }
};

void speak(Animal &animal) {
    animal.speak();
}

void eat(Animal& animal) {
    animal.eat();
}

int main()
{
    Cat cat;
    speak(cat); // 喵喵喵
    eat(cat);   // 期望“猫在吃”,实际“动物在吃”

    Dog dog;
    speak(dog); // 汪汪汪
    eat(dog);   // 期望“狗在吃”,实际“动物在吃”
}

多态原理

多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不能确定到底调用的是基类还是派生类的函数,运行时才能确定。

每一个有「虚函数」的类(或有虚函数的类的派生类)都有一个「虚函数表」,该类的任何对象中都放着虚函数表的指针(vfptr)。「虚函数表」中列出了该类的「虚函数」地址。

纯虚函数和抽象类

多态中,基类中的虚函数的实现通常是无意义的,主要都是调用子类重写的函数,因此可以将基类中的虚函数写为纯虚函数(未给出有意义实现的函数)

实现纯虚函数的方法是在函数原型后加 =0

virtual void func() = 0;

当类中有纯虚函数,这个类就是抽象类

抽象类特点:

  • 无法实例化对象
  • 其派生必须重写抽象类中的纯虚函数,否则也属于抽象类

示例:

#include <iostream>

class Base {
public:
    virtual void func() = 0;
};

class Drived: public Base {
public:
    void func() {
        std::cout << "重写了 func()" << std::endl;
    }
};

int main()
{
    // Base b; // 抽象类无法实例化
    Drived d;  // 重写了纯虚函数的派生类可以实例化 
}

虚析构和纯虚析构

使用多态时,若子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。

解决方式:将父类中的析构函数改为虚析构纯虚析构

虚析构和纯虚修够的共性:

  • 可以解决父类指针无法释放子类对象的问题
  • 都需要有具体实现

区别:

  • 若类有纯虚析构,则该类未抽象类,无法实例化对象

示例:

#include <iostream>
#include <string>

using namespace std;

class Animal {
public:
    Animal() {
        cout << "Animal 构造函数调用" << endl;
    }
    // 若 Animal 不是虚函数,则 Cat 的析构函数无法被调用
    virtual ~Animal() {
        cout << "Animal 析构函数调用" << endl;
    }

    // 若使用下面的纯虚析构,需要具体实现
    // virtual ~Animal() = 0;
    virtual void speak() = 0;

    string* m_name;
    
};

class Cat : public Animal {
public:
    Cat(string name) {
        cout << "Cat 构造函数调用" << endl;
        m_name = new string(name);
    }

    void speak() {
        cout << *m_name << " 小猫在说话" << endl;
    }

    ~Cat() {
        cout << "Cat 析构函数调用" << endl;
        if (m_name != NULL) {
            delete m_name;
            m_name = NULL;
        }
    }

    string* m_name;
};

void test() {
    //Animal* cat = new Cat("Tom");
    //cat->speak();
    //delete cat

    Cat cat("Tom");
    cat.speak();
}
int main()
{
    test();
}

输出:

Animal 构造函数调用
Cat 构造函数调用
Tom 小猫在说话
Cat 析构函数调用
Animal 析构函数调用
posted @ 2021-02-14 23:44  Lambyte  阅读(45)  评论(0)    收藏  举报