C++ 面向对象

408 阅读12分钟

面向对象就是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统。

抽象和类

抽象指的是从具体事物抽取共同的本质特征。

  • 将抽象转换为用户定义类型的工具
  • 将数据表示和操纵数据的方法组合成一个整体
  • 类的实例称为对象
  • 类中的变量和函数称为成员

类的声明

使用 class/struct 关键字声明类型。
例如:

class 类名{};
class LandOwner{};
struct 类名{};
struct Hero{};

需要注意的是:

  1. class 方式声明的类型与 struct 声明的类型仅仅是形式上不同
  2. 唯一的区别在于使用 class 声明的类型默认成员是私有的(private),而 struct 声明的类型默认成员是共有的(public
  3. 在 C++ 中 struct 仅用来存放数据,即 POD(Plain old data structure),这是 C++ 语言的标准中定义的一类数据结构。

类的创建

C++ 中类的创建一般使用单独一个 class.h 文件来创建一个类和声明其方法,通过 class.cpp 来实现类的方法等。
若想要在一个文件中完成类的创建和方法实现,则使用 class.hpp 文件进行保存,实现原理是这种文件格式内的函数默认为内联函数,所以可以直接通过单个文件实现。
.h 文件中若想要直接实现方法,则需要使用 inline 关键字声明一个函数方法。
例如一个 .hpp 的类:

// LandOwnerV1.hpp文件
#pragma once
#include <iostream>
using namespace std;
class LandOwnerV1
{
private:
	string name;		//地主的名称
	long score;			//地主的积分
	int cards[20];		//地主的手牌数组
public:
	void touch_card(int cardCount) {
		cout << name << "摸了" << cardCount << "张牌" << endl;
	}
};

.h 文件和 .cpp 文件创建一个类:

// LandOwnerV2.h 文件
#pragma once
#include <iostream>

using namespace std;

class LandOwnerV2
{
private:
	string name;		//地主的名称
	long score;			//地主的积分
	int cards[20];		//地主的手牌数组
public:
	void touch_card(int cardCount);
	void show_score();
};
// LandOwnerV2.cpp 文件
#include "LandOwnerV2.h"
// 在.cpp 文件中定义函数时一定要加上 类名::函数名
void LandOwnerV2::touch_card(int cardCount) {
	cout << name << "摸了" << cardCount << "张牌" << endl;
}

其中在 .cpp 文件中定义函数时,一定要加上 类名::函数名 其中 :: 是作用域解析运算符,用来声明该函数是属于这个类的。

访问修饰符

访问修饰符就是定义类中定义类成员的访问限制的关键字,常见的有:

  • public :修饰的成员在任意地方都可以访问
  • private :修饰的成员只能在类中或友元函数中访问
  • protected :修饰的成员可以在类中函数、子类函数及友元函数中访问

用法:默认没有修饰符成员为 private

class 类名{
修饰符:
    成员列表;
};

构造函数

所谓的构造函数就是在类创建时,编译器自动调用的初始化函数,类似于 Python 中的 __init__() 函数。其相关特征如下:

  • 特点
    • 以类名作为函数名
    • 无返回值类型
  • 作用
    • 初始化对象的数据成员
    • 类对象被创建时,编译器为对象分配内存空间,并且自动调用构造函数来完成成员的初始化
  • 种类
    • 无参构造
    • 一般构造(重载构造)
    • 拷贝构造
    • 转换构造

构造函数的几种定义与调用

Student.h 文件

// Student.h文件
#pragma once
#include <iostream>
using namespace std;
class Student
{
	string _name;
	string _desc;
	int _age;
public:
	Student();					//默认构造
	Student(int);				//带参构造
	Student(string, string);	//带参构造
	void show_info();
};

Student.cpp 文件

// Student.cpp文件
#include "Student.h"

Student::Student() {
	cout << "Student() 默认构造正在被调用!" << endl;
}
Student::Student(int age) {
	cout << "Student(int age) 带参构造正在被调用!" << endl;
	_age = age;
}
// 这种初始化赋值还可以采用下面的简便写法
Student::Student(string name, string desc) : _name(name), _desc(desc)
{
	cout << "Student(string name, string desc) 带参构造正在被调用!" << endl;
}

void Student::show_info() {
	cout << "名字:" << _name << endl;
	cout << "名言:" << _desc << endl;
	cout << "年龄:" << _age << endl;
}

main() 函数文件

#include <iostream>
#include "Student.h"
int main()
{
    Student stu1;       //调用默认构造
    Student stu2(20);
    stu2.show_info();
    Student stu3 = 40;  //当一个构造函数只带一个参数,并且它是所有构造函数中唯一的话,可以这样定义
    stu3.show_info();
    Student* stu4 = new Student("撒贝宁", "北大还行");
    stu4->show_info();
    delete stu4;
    
    return 0;
}

输出结果:

Student() 默认构造正在被调用!
Student(int age) 带参构造正在被调用!
名字:
名言:
年龄:20
Student(int age) 带参构造正在被调用!
名字:
名言:
年龄:40
Student(string name, string desc) 带参构造正在被调用!
名字:撒贝宁
名言:北大还行
年龄:-842150451

拷贝构造

思考一个问题,当我们的自定义类型带有指针或其他一些资源时,然后当我们想使用一个实例去初始化另一个实例时,会发生什么。这时会将实例的地址复制给新的实例,也就是说两个实例不是独立的,而是同一个对象。
使用 = 赋值也是一样,这是所谓的浅复制。
对于 = 赋值,我们可以使用重载赋值运算符的方法:

// string包装类的例子
String & String::operator=(const String & str) {
    delete[] this->value;     // 释放原字符串的空间
    len = strlen(str.value);
    this->value = new char[len + 1];
    strcpy(this->value, str.value);     // 拷贝字符串
    return *this;
}

那么对于另一种情况,我们就可以定义复制构造函数来解决。
所谓的复制构造就是将一个自定义类型的实例复制给一个新的自定义类型实例,他会在以下三种情况下调用:

  1. 当类的对象被初始化为同一类的另一个对象时
  2. 当对象被作为参数传递给一个函数时
  3. 当函数返回一个对象时
// 在以下情况会调用复制构造函数
void func (String);     // 传入一个String类型的数据副本
String func ();         // 返回一个String
类型的数据副本
String str1("你好");
String str2(str1);      // 使用str1初始化str2

解决办法就是,重载构造函数:

String::String(const String & str) {
    len = strlen(str.value);
    this->value = new char[len + 1];
    strcpy(this->value, str.value);     // 拷贝字符串
}

转换构造

所谓的转换构造其实就是,当定义自定义类型时,只传入一个参数所调用的构造函数。常常用在编译器需要自动进行类型转换的时候。例如定义一个 Rectangle 类型:

class Rectangle
{
	double _width, _height;
public:
	Rectangle() : _width(0), _height(0) {}		// 默认(无参)构造
	Rectangle(double width, double height) : _width(width), _height(height) {}		// 带参构造
	Rectangle(const Rectangle& rect) {}		// 复制构造
	Rectangle(double width) : _width(width), _height(0) {} // 转换构造
	// 或者可以将默认构造、转换构造、带参构造写成下面的间便写法
	//Rectangle(double width = 0, double height = 0) : _width(width), _height(height) {}
};
int main() {
    Rectangle rect1();  // 调用默认构造
    Rectangle rect2(20, 30);    // 调用带参构造
    Rectangle rect3(rect2);     // 调用复制构造
    Rectangle rect4(20.5);      // 调用转换构造
    Rectangle rect5 = 33.3;     // 调用转换构造
    Rectangle rect6;
    // 下面的语句会尝试将'A' 和 false 先转换成 double 类型。
    // 然后在调用 Rectangle 的转换构造,将其转换成 Rectangle 类型。
    // 最后再尝试去执行 Rectangle 类型的加法运算。
    rect6 = rect5 + 'A' + false;
}

析构函数

析构函数和构造函数是一对,它是对象过期时自动调用的特殊函数。
析构函数的定义时在类名前加上 ~
注意: 析构函数没有参数,并且只能有一个。
例如:

class Student {
    double* scores;
public:
    Student(int len) {
        scores = new double[len];
    }
    ~Student() {
        delete scores;
    }
};

this 关键字

this 是一个指针,它存在于成员函数当中,类似于 Python 中的 self 参数。
类中的每一个成员函数(包括构造和析构)都有一个 this 指针,this 指针指向调用对象,即可以通过 this 关键字访问当前对象的成员。
this 指针是类成员函数的第一个默认隐含参数,和Python 中的 self 一样是会默认传递的,不同的是C++中不能写出来。 例如:

// C++ 中写法
Student::set_name(string name) {
    this->_name = name;
}
// C 中写法
set_name(const Student* thiz) {
    thiz->_name = name;
}

注意点:

  1. this 指针本身不占用大小,他并不是对象的一部分,因此不会影响 sizeof 的结果
  2. this 的作用域在类成员函数的内部
  3. this 指针是类成员函数的第一个默认隐含参数,编译器自动维护传递,类编写者不能显示传递
  4. 只有在类的非静态成员函数中才可以使用 this 指针,其他任何函数都不可以

运算符重载

这里需要理解一个概念,运算符例如 +-*/ 其实就是简便的调用一些内部的标准函数,所谓的运算符重载就是重新定义这些标准函数,从而让运算符能够实现不同的功能。
这很类似于 Python 中的 __add__(self, other) 这种内置函数,通过重新定义,可以让 + 号实现自定义的功能。
使用方法如下:

#pragma once
#include <iostream>
class Integer
{
	int _value;
public:
	Integer(int value = 0) : _value(value) {}
	int get_value() { return _value; }
	Integer operator+(const Integer& other) const {
		std::cout << "调用重载运算符+,实现两个Integer对象相加" << std::endl;
		return (this->_value + other._value);
	}
};

重载运算符的范围

可以被重载的运算符如下:

类型 运算符
二元运算符 +, -, *, /, %
关系运算符 ==, !=, <, >, <=, >=
逻辑运算符 ||, &&, !
一元运算符 +(正), -(负), *(指针), &(取地址), ++, --
位运算符 |(按位与), &(按位或), ~(按位取反), ^(按位异或), <<(左移), >>(右移)
赋值运算符 =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
内存声明与释放 new, delete, new[], delete[]
其他运算符 ()(函数调用), ->(成员访问), ->*(成员指针访问), ,(逗号), [](下标)

不能够被重载的运算符有5个:

  • .(点运算符)
  • *(成员指针访问运算符)
  • ::(域运算符)
  • sizeof(长度运算符)
  • ? :(三元运算符/条件运算符)

重载注意点

  1. 重载不能够修改运算变量的个数
    例如:二元运算符一定是有两个变量参数参与运算的。

  2. 重载不能修改运算符的优先级别
    例如:*/ 优先于 +-,重载之后也不会变化。

  3. 重载不修改运算顺序
    例如:= 赋值运算永远是从右到左,重载也不会改变。

友元函数

在C++中,我们使用类对数据进行了隐藏和封装,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。

友元函数是可以直接访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但需要在类的定义中加以声明,声明时只需要在友元函数的开头加上关键字 friend
例如:

friend 返回值类型 函数名(参数);

重载运算符友元函数的使用

在重载运算符时,如何判断是使用类成员还是友元函数呢?有如下的准则:

  • C++规定,赋值运算符 =、数组下标运算符 []、函数调用运算符 ()、成员访问运算符 -> 在重载时必须声明为类的成员函数
  • 流运算符 <<>>、类型转换运算符不能定义为类的成员函数,只能是友元函数
  • 一元运算符和符合赋值运算符重载时,一般声明类的成员函数
  • 二元运算符在运算符重载时,一般声明为友元函数

注意: 在定义运算符时,必须选择其中一种格式,而不能同时选择这两种格式,否则有可能导致二义性错误

例如重载 << 实现类的直接输出:

#include <iostream>
class Integer
{
	int _value;
public:
	Integer(int value = 0) : _value(value) {}
	friend ostream& operator<<(ostream& out, const Integer& num);
};
ostream& operator<<(ostream& out, const Integer& num) {
    out << num._value;
    return out;
}
int main() {
    Integer num = 1024;
    cout << num << endl;
}

输出结果:

1024

类型转换函数

当一个其他类型的数据想要转换成一个自定义类型的时候,会尝试去调用自定义类型的转换构造。而如果想要将一个自定义类型转换成其他类型的话需要如何操作呢?

这时候就需要重载类型转换运算符了,这个函数也称为类型转换函数,他有如下几个特点:

  • 类型转换函数就是将当前类类型转换为其他类型
  • 他只能以成员函数的形式出现
  • 类型转换函数看起来没有返回值类型,其实是隐式地指明了返回值类型
  • 类型转换函数也没有参数,其实是使用隐式传递的 this 指针参数进行操作
...
operator float() const {
	return this->_width;
}
...
float rect_width = float(rect2);
cout << "rect_width = " << rect_width << endl;
...

输出结果:

rect_width = 20

注意:

  1. type 可以是内置类型,类类型和由 typedef 定义的类型别名,任何作为函数返回类型 **(void 除外)**都是被支持的
  2. 不允许转换为数组或函数类型,可以转换为指针或引用类型
  3. 类型转换函数一般不会更改被转换的对象,所以通常被定义为 const 函数
  4. 类型转换函数可以被继承,可以是虚函数