面向对象编程 简单的自学
OOP的一些基本概念
oop:类把对象的数据和操作数据的方法作为一个整体考虑
类的成员可以是变量,也可以是函数。
类的成员变量也叫属性。
类的成员函数也叫方法/行为,类的成员函数可以定义在类的外面。
1 |
|
用类定义一个类的变量叫做创建(或实例化)一个对象。
类的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同。
类的访问权限
public
private
protected
区别体现在类的外部或派生类
public
: 外部可以访问(通过对象),派生类可以访问。protected
: 外部 不能访问,派生类 可以访问。private
: 外部 不能访问,派生类 也不能直接访问。
在类的内部(类的成员函数中),无论成员被声明为public
还是private
,都是可以访问。
在类的外部(定义类的代码之外),只能访问public
成员,不能访问private
、protected
成员。
在一个类体的定义中,private
和public
可以出现多次。
结构体的成员缺省为public
,类的成员缺省为private
。
private
的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public
。
类的简单使用
1)类的成员函数可以直接访问该类其它的成员函数。
2)类的成员函数可以重载。
(普通函数具备的特征成员函数都具备)
3)类指针的用法与结构体指针用法相同。
4)类的成员可以是任意数据类型 (类中枚举)。
1 |
|
5)可以为类的成员指定缺省值(C++11标准)。
6)类可以创建对象数组
7)对象可以作为实参传递给函数,一般传引用。
8)可以用new
动态创建对象,用delete
释放对象。
9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。
10)对象一般不用memset()
清空成员变量,可以写一个专用于清空成员变量的成员函数。
11)对类和对象用sizeof
运算符意义不大,一般不用。
12)用结构体描述纯粹的数据,用类描述对象。
13)类的分文件编写。
构造函数与析构函数
构造函数就是在构造一个类时,将该类的所有成员初始化的函数;析构函数则是在一个类结束生命时(函数结束或者被delete
),将所有的类成员消灭,回收它们占有的空间的函数。
构造函数与类同名,没有返回类型,构造时自动调用,可以重载。可以用于初始化,不能直接调用。
析构函数也没有返回类型,析构时自动调用,没有参数,不能重载。不提倡主动调用析构函数。
构造函数:在创建对象时,自动进行初始化
析构函数:在销毁对象前,自动进行清理工作
构造函数
语法:类名(){}
访问权限必须是public
。
函数名必须与类名相同。
没有返回值,也不写void
。
可以有参数,可以重载,可以有默认参数。
创建对象时会自动调用一次,不能手工调用。
初始化应该使用成员初始化列表
1 |
|
确保初始化列表中的顺序与成员变量在类中的声明顺序一致
析构函数
语法 ~类名(){}
访问权限必须是public
名字要在类名前面加~
没有返回值,不写void
没有参数,无法重载
销毁对象前只会自动调用一次,但是可以手动调用
拷贝构造函数
把某一个对象的成员变量赋值给一个新的对象,不会调用构造函数
编译器默认会提供
语法 类名 (const 类名& 对象名){}
函数传对象的形参的时候会调用拷贝析构函数
浅拷贝与深拷贝
如果让一个A
类型的类进行A b=a;
这样的操作,则会调用(默认的)拷贝构造函数A(const A&)
,为b
初始化。
调用拷贝构造函数产生的效果分为浅拷贝和深拷贝。浅拷贝指的是只拷贝地址,深拷贝则是拷贝了地址上的内容。
初始化列表
在构造函数函数体前面可以采用类似于A():a(b),c(d)
的操作,称为成员初始化列表。
成员初始化列表不同于赋值。赋值是在对象存在以后再进行操作,而初始化则是直接在对象构造完成前进行操作。
对于引用和const
成员的初始化,只能在成员初始化列表中实现。
初始化列表与赋值有本质的区别,如果成员是类,使用初始化列表调用的是成员类的拷贝构造函数,而赋值则是先创建成员类的对象(将调用成员类的普通构造函数),然后再赋值。
不能在成员初始化列表中初始化静态数据成员
const
修饰成员函数
在类的成员函数后面加const
关键字,表示在成员函数中保证不会修改调用对象的成员变量。
在成员变量前面加上mutable
关键字,即使是const
修饰过的成员函数也可以修改调用对象的成员变量。
非const
成员函数可以调用const
成员函数和非const
成员函数
const
成员函数不能调用非const
成员函数。
const
对象只能调用const
成员函数
给构造函数和析构函数加上const
修饰符是非法的。
this
指针
如果类的成员函数中涉及多个对象,需要使用this
指针
this
指针存放了对象的地址,它被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用者对象)。
每一个成员函数(包括构造函数与析构函数)都有一个this
指针,可以用于访问调用者对象的成员。可以解决成员变量名与函数形参名相同的问题。
1 |
|
静态成员
包括静态成员变量和静态成员函数
可以实现多个对象之间的数据共享
普通对象的成员变量要先创建对象然后才能访问,静态成员变量不创建对象也能访问,而且必须在程序的全局区进行初始化,需要使用类的命名空间。
如果把类的成员声明为静态的,就可以把它和类的对象独立开来。(静态成员不属于对象)
类的公有静态成员的性质与全局变量和全局函数相同。
类的静态成员函数只能访问类的静态成员而不能访问非静态成员。
静态成员函数中没有this
指针
类的私有静态成员在类外无法访问
const
静态成员变量可以在定义类的时候初始化,而不加const
限定符则不行。
简单对象模型
成员函数、成员变量、静态成员函数、静态成员变量,都是在内存中分散分布的。
在没有创建对象的前提下,访问非静态成员变量就是访问空指针(这个时候this
指针是nullptr
)
为了程序的鲁棒性,可以在成员函数中增加判断this
指针是否为空的代码
友元
友元提供了另一种访问类的私有成员的方案
在 C++ 标准里,友元函数(friend
function)永远不是类的成员函数,即使你在类里面写了它的定义。
C++ 标准规定:
友元函数是授予访问权限的外部函数,不属于类的成员函数。
友元全局函数
在友元全局函数中可以访问另一个类的所有成员
声明友元的代码放在类中哪个位置都可以,一般放在最上面
1 |
|
友元类
友元关系不能被继承
友元关系是单向的
1 |
|
友元成员函数
在友元成员函数中可以访问另一个类的所有成员
1 |
|
注意两点:
- 先前向声明类
B
,否则无法识别B::showA
- 在类
A
中用friend
声明B::showA
,使它能访问A
的私有成员
运算符重载
语法返回值 operator运算符(参数列表)
运算符重载函数的返回值类型要与运算符本身的含义一致
非成员函数版本的重载运算符函数:形参个数与运算符的操作符个数相同
成员函数版本的重载运算符函数:多一个
1 |
|
如果想要实现1+a,就必须写成非成员函数(或友元)重载
而且这里的+的重载也不太对,应该重载+=更合适
1 |
|
this
是 T*
,所以 *this
是一个左值
返回类型如果是 T&
,return *this
就是返回当前对象的引用
C++ 里左值(lvalue)和右值(rvalue)是表达式分类的核心概念,理解这个概念后,你就能明白为什么
return *this
能返回引用、为什么某些表达式可以取地址而有些不行。下面我帮你用简单的语言和例子详细解释。
1️⃣ 定义
左值(lvalue)
- lvalue = location value
- 可以表示内存中有明确地址、可以取地址的对象
- 通常可以出现在赋值号左边(所以叫左值)
例如:
1
2
int x = 10;
int* p = &x; // ✅ x是左值,有地址
右值(rvalue)
- rvalue = read value
- 临时值或字面量,没有稳定的内存地址(无法取地址)
- 通常只能出现在赋值号右边
例如:
1
2
3
int x = 10;
10 = x; // ❌ 10是右值,不能赋值
int* p = &10; // ❌ 字面量没有可取的地址
2️⃣ 常见示例
1
2
3
4
5
int a = 5;
int b = 10;
a = b; // ✅ a是左值,b是左值(表达式结果是右值)
a = a + b; // a是左值,(a+b)是右值
- 左值表达式:变量名、解引用
*p
、返回引用的函数调用- 右值表达式:字面量、算术表达式、返回值(非引用)的函数调用
3️⃣ 左值、右值与函数返回
1
2
3
4
5
6
7
int x = 5;
int& foo1() { return x; } // 返回左值引用
int foo2() { return x; } // 返回值
foo1() = 10; // ✅ 返回左值引用,可以赋值
foo2() = 10; // ❌ 返回右值,临时对象不可赋值这也解释了为什么运算符重载中:
operator+=
返回T&
,因为我们希望它是左值,支持链式赋值operator+
返回T
,因为结果是临时值,应该是右值总结口诀
- 左值:能取地址,有名字,能放在
=
左边- 右值:临时值,没稳定地址,通常只能放在
=
右边- 返回引用 → 左值
- 返回值 → 右值(除非是
static
局部或优化)
重载关系运算符
== != > < >= <=
重载左移运算符
重载左移运算符只能使用非成员函数版本
要输出类的私有成员可以使用友元
1 |
|
重载下标运算符
[]
如果对象中有数组,重载下标运算符,操作对象中的数组将像操作普通数组一样方便。
语法
返回值类型& operator[] () {}
和
const 返回值类型& operator[] () {}
在实际开发中,这两种要同时提供,目的是为了适应const
对象,因为const
对象只能访问const
函数
在重载函数中可以对下标进行合法性检查防止数组越界
1 |
|
重载赋值运算符
默认存在
重载之后就不调用默认的
重载new
delete
运算符
*void
表示一块与数据类型无关的内存空间
重载括号运算符
括号运算符函数只能用类的成员函数重载,不能用全局函数
可以实现一个仿函数
什么都和函数一样,但是就本质和函数不一样
在模板和STL中有广泛的应用
重载一元运算符
主要是自增自减运算符,因为放在左边右边都可以
C++规定,重载++和--时,如果重载函数有一个int
形参,编译器处理后置表达式的时候将调用这个重载函数
1 |
|
自动类型转换
把某种数据类型转换为类的类型
在C++中,将一个参数的构造函数用作自动类型转换函数,它是自动进行的,不需要显式的转换。
1 |
|
- 一个类可以有多个转换函数
- 多个参数的构造函数,除第一个参数外,如果其它参数都有缺省值,也可以作为转换函数。
- 如果自动类型转换有二义性,编译将报错
将构造函数用作自动类型转换函数似乎是一项不错的特性,但有时候会导致意外的类型转换。explicit
关键字用于关闭这种自动特性,但仍允许显式转换。
转换函数
把类转换为某种类型
语法 operator 数据类型()
1 |
|
注意:转换函数必须是类的成员函数;不能指定返回值类型;不能有参数。
如果隐式转换存在二义性,编译器将报错
还有一种方法是:用一个功能相同的普通成员函数代替转换函数,普通成员函数只有被调用时才会执行。这是比较推荐的。
举个例子:
1 |
|
[!WARNING]
应谨慎地使用隐式转换
类的继承
继承:一个类从另一个类获得成员的过程
被继承的类:基类或父类
继承的类:派生类或子类
语法
1 |
|
继承方式
public
protected
private
当类 B 继承 自类 A 时,基类成员在派生类中的权限会受到继承方式的影响:
基类成员 public继承 protected继承 private继承 public
派生类中仍是 public
派生类中变成 protected
派生类中变成 private
protected
派生类中仍是 protected
派生类中仍是 protected
派生类中变成 private
private
无法继承(不可见) 无法继承(不可见) 无法继承(不可见) 📌 注:无论什么继承方式,基类的
private
成员永远不会被继承到派生类中(即使继承了也访问不到)。
由于private和protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以,在实际开发中,一般使用public。
在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。
使用using关键字可以改变基类成员在派生类中的访问权限。
1 |
|
[!CAUTION]
注意:using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为基类中private成员派生类中是不可见的,根本不能使用。
类继承的对象模型
创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。
创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间,this指针是相同的。
创建派生类对象时,先初始化基类对象,再初始化派生类对象。
如何构造基类
派生类构造函数的要点如下:
创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
如果没以指定基类构造函数,将使用基类的默认构造函数。
可以用初始化列表指明要使用的基类构造函数。
基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。
派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
基类的成员变量必须由基类的构造函数初始化!
名字遮蔽与类作用域
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的。
基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数。
1 |
|
在成员名前面加类名和域解析符可以访问对象的成员。如果不存在继承关系,类名和域解析符可以省略不写。
当存在继承关系时,基类的作用域嵌套在派生类的作用域中。如果成员在派生类的作用域已经找到,就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找。(为什么会有名字遮蔽)
如果在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员。
继承的特殊关系
派生类和基类之间有一些特殊关系:
如果继承方式是公有的,派生类对象可以使用基类成员。
可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员。
基类指针可以在不进行显式转换的情况下指向派生类对象。
基类引用可以在不进行显式转换的情况下引用派生类对象。
多继承与虚继承
class C : public A, public B {};
菱形继承:
A有两个子类B, C,D继承了B, C
会有二义性,冗余
加上virtual
关键字
class C : virtual public A, virtual public B {};
类的多态
基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
如果在基类的成员函数前加virtual关键字,把它声明为虚函数,基类指针就可以调用派生类中同名的成员函数,通过派生类中同名的成员函数,就可以访问派生对象的成员变量。
有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生类的成员函数和数据,基类指针表现出了多种形式,这种现象称为多态。
基类引用也可以使用多态。
[!CAUTION]
只需在基类的函数声明中加上virtual关键字,函数定义时不能加。
在派生类中重定义虚函数时,函数特征要相同。
当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。
名字遮蔽和重载函数的规则也适用于虚函数。
在派生类中重定义了虚函数的情况下,如果想使用基类的函数,可以加类名和域解析符。
如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。
多态的应用场景
- 基类的虚函数实现基本功能
- 派生类重定义虚函数,扩展功能、提升性能、实现个性化功能
如何析构派生类
派生类的析构函数在执行完后,会自动执行基类的析构函数
为了实现多态的稳定性,把基类的析构函数设置为虚函数
构造函数和析构函数不能继承
赋值运算符函数和友元函数不能继承
析构函数可以手动调用,如果对象中有堆内存,析构函数中此代码是必要的:
1 |
|
对于基类而言,即使它不需要析构函数,也应该提供一个空的虚析构函数
纯虚函数与抽象类
纯虚函数:基类中的函数不需要任何缺省与功能
1 |
|
含有纯虚函数的类被称为抽象类,不能实例化对象,可以创建指针和引用
纯虚析构函数一定要有代码实现,意义在于有时候想让一个类成为抽象类,但是又刚好没有任何纯虚函数,怎么办?方法:在想要成为抽象类的类中声明一个纯虚析构函数。
运行阶段类型识别与dynamic_cast
关键字
运行阶段类型识别(RTTI RunTime Type Identification)为程序在运行阶段确定对象的类型,只适用于包含虚函数的类。
dynamic_cast
运算符将指向基类的指针生成指向派生类的指针
语法
派生类指针 = dynamic_cast<派生类类型 *> (基类指针)
dynamic_cast
只适用于包含虚函数的类
typeid
运算符与type_info
类
typeid
运算符
- 是 C++ 提供的一个运行时类型识别(RTTI)机制。
- 用法:
typeid(expression)
- 返回一个
const std::type_info&
对象,描述表达式的真实类型。
type_info
类
- 是
<typeinfo>
头文件中定义的一个类,封装了类型信息。 - 你不能直接创建
type_info
对象,但可以通过typeid
获得它的引用。 - 常用方法:
name()
:返回一个平台相关的类型名(通常是 mangled,需要用abi::__cxa_demangle
解码)。before()
:用于排序类型。operator==
、operator!=
:比较类型是否相同。
函数模板
函数模板是通用的数据类型,用任意数据类型(泛型)来描述函数
生成函数定义的过程被称为实例化
1 |
|
如果不想让编译器自动推导数据类型,可以这样写:
1 |
|
函数模板的注意事项
可以为类的成员函数创建模板,但不能是虚函数和析构函数。
使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。
使用函数模板时,推导的数据类型必须适应函数模板中的代码。
使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换。
函数模板支持多个通用数据类型的参数。
1 |
|
函数模板支持重载,可以有非通用数据类型的参数。
函数模板的具体化
可以提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。
语法
template <> 函数定义(非通用数据类型的参数列表)
编译器使用函数的规则顺序:普通函数,函数模板,特化函数模板,重载函数
函数模板高级 (cpp11+)
decltype
关键字
操作符,用于查询表达式的数据类型
语法 decltype(expression) var;
这个操作符会返回数据类型,可以用它定义变量
比如decltype(1+1) a;
等价于int a;
这个操作符只会分析类型,不会执行表达式
一些规则:
如果expression是没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符。
如果expression是函数调用,则var的类型与函数的返回值类型相同(函数不能返回void,但可以返回void*)
如果expression是左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那么var的类型是expression的引用。
左值的情况:
1
2int a = 1;
decltype(++a) b; //b的类型是int&括号的情况:
1
2
3int a = 1;
decltype(a) b; //b的类型是int
decltype((a)) b; //b的类型是int&
如果上面的条件都不满足,则var的类型与expression的类型相同
函数后置返回类型
1 |
|
这里的auto
相当于一个占位符
这个意义在于函数模板返回值未定的情况下
1 |
|
C++14的auto
关键字
cpp14做了一个优化,函数可以直接用auto
当返回值,不需要尾随返回类型
1 |
|