面向对象程序设计
一、构造函数
成员函数定义
初始化列表
1 | class Clock { |
- new/delete
1 | A *p = new A[3]; |
- 默认构造函数
1 | class MyClass { |
- 默认构造函数&缺省参数初始化
默认构造函数是指不带参数(MyClass() {}
)或所有参数都有默认值的构造函数(MyClass() : data(0) {}
)。
1 |
|
如果类中定义了参数化构造函数,而没有定义默认构造函数,则编译器不会自动生成默认构造函数。这意味着,如果你需要在没有提供参数的情况下创建对象,就必须显式地定义默认构造函数。
1 | class MyClass { |
这个例子中,你可以通过 MyClass obj;
来创建一个 MyClass
的对象,就像有一个默认构造函数一样。
缺省参数(也称为默认参数)是函数参数在声明时指定的默认值。
1 | // 函数声明,其中 b 和 c 是缺省参数 |
从右向左:缺省参数必须从右向左依次声明,不能跳过中间的参数。
- 日期类
1 | //获取某年某月的天数 |
- 拷贝构造函数
1 |
|
- 构造函数的转换构造函数
当构造函数只有一个参数时,C++ 允许使用这个构造函数进行隐式类型转换。这种构造函数被称为转换构造函数。
例如,假设我们有一个类 A
,它有一个接受 int
参数的构造函数:
1 | class A { |
在这种情况下,我们可以使用 int
来创建 A
类的对象,就像这样:
1 | A a = 10; // 隐式调用 A(int) 构造函数 |
二、内联函数inline()
使用 inline
关键字声明或定义的函数被称为内联函数。一般情况下编译器会用内联函数的代码直接替换函数调用(内联展开),这样就省去了函数调用时的跳转以及返回等操作,因此内联函数的运行速度比常规函数稍快,
一般情况下,在类定义中的定义的函数都是内联函数,而在类外定义是需要通过 inline
关键字显式指定。
1 | inline int getMax(int a, int b) { |
三、引用
常引用
- 例子
1 | wrong: |
int & a = 10;
这行代码声明了一个对整数类型的引用a
并将其初始化为值10
。这里的10
是一个右值(rvalue
),指的是一个临时的、不可重复使用的值,而不是一个可以被赋值的持久对象(左值,lvalue
)。这里的
const
关键字表明a
是一个只读引用,它不能被用来修改它所引用的对象。
- 下例类比
1 | int fun(){ |
四、const
修饰
const
修饰成员函数,该函数只能访问而不能修改成员变量,如果需要修改时,需要用mutable
修饰该变量。const
修饰成员变量
必须在构造函数的初始化列表中进行初始化,因为它们在对象创建后就不能再被修改。这就意味着,你不能在构造函数的主体中或者在其他任何地方修改常量成员变量的值。
1 | class MyClass { |
const
修饰对象,称之为常对象,只能调用const
修饰的成员函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class MyClass {
public:
int value; // 默认为非const成员变量
MyClass(int v) : value(v) {}
// 普通成员函数,可以修改成员变量
void setValue(int v) {
value = v;
}
// const成员函数,不能修改非mutable成员变量
int getValue() const {
return value;
}
};
int main() {
const MyClass obj(10); // 创建一个const对象
// 调用const成员函数
int val = obj.getValue(); // 正确
cout<<val;
// 不能调用非const成员函数
obj.setValue(20); // 错误
return 0;
}
非常量对象可以调用其常量成员函数和非常量成员函数。
然而,常量对象只能调用其常量成员函数,不能调用非常量成员函数。
const
和 #define
的区别:
特性/关键字 | const |
#define |
---|---|---|
处理阶段 | 编译、运行阶段 | 预处理阶段 |
类型检查 | 执行类型检查 | 不进行类型检查 |
调试支持 | 支持调试 | 无法调试 |
字符串替换 | 不是简单的字符串替换 | 简单的字符串替换 |
边界效应 | 不存在边界效应 | 存在边界效应 |
内存分配 | 可能需要内存分配 | 不需要内存分配 |
五、静态成员
静态成员变量:静态成员变量存储在全局数据段。它们是类的所有对象共享的,即类的所有实例都会共享同一份静态成员变量。静态成员变量需要在类定义外部进行初始化。
静态成员函数:静态成员函数存储在代码段。它们不属于类的任何一个对象,而是属于类本身。静态成员函数可以在没有类的对象的情况下被调用,只需要使用类名和作用域解析运算符
::
==没有this指针==静态数据成员被存放在==静态存储区==
静态数据成员所占的空间不会随着对象的产生而分配,也不会随着对象的消失而回收。只有在程序结束时才被系统释放。
- 必须初始化
1 | private: |
- 在静态成员函数中可以直接引用其静态成员,而引用非静态成员时需用对象名引用。
当在类外部使用 X::func()
语法调用某个函数时,这个函数 func
必须是类 X
的静态成员函数。
1 | class X { |
六、友元
6.1友元函数
MyClass
有一个私有成员 value
。showValue
函数被声明为 MyClass
的友元,这意味着它可以访问 MyClass
的私有成员
1 | class MyClass { |
6.2友元类
1 | class A { |
七、运算符重载
运算符重载可以通过成员函数重载或者友元函数重载实现。
运算符
::
、?:
、.
、.*
、sizeof
、typeid
、const_cast
、dynamic_cast
、reinterpret_cast
、static_cast
不允许重载。双目运算符
=
、()
、[]
、->
与类型转换运算符只能以成员函数方式重载,流运算符<<
与>>
只能以友元函数的方式重载。将运算符函数定义为成员函数时,调用成员函数的对象(this指向的对象)被作为运算符的第一个操作数,所以如果是一元运算符,无需提供参数。
如果将运算符函数定义为全局函数,则通常要将其声明为类的友元函数。
const Byte& operator+( ) const {}
- 第一个
const
:位于成员函数声明的左侧,它修饰的是返回类型。这里的const
表示返回值是一个常量引用,即它不能被修改。 - 第二个
const
:位于成员函数声明的右侧,它修饰的是成员函数本身。这个const
关键字表示这个函数是一个常量成员函数,它不会修改调用它的对象的状态。
- 第一个
7.1 成员函数重载一元运算符
1 | Date&operator++() |
Date&operator++()
重载前置++
Date&operator++(int)
重载后置++;
- 此处的int没有意义。只是用于区分。系统调用时传0。
7.2 用友元函数重载一元运算符
1 | friend const Integer& operator++(Integer& a); // 前缀++ |
全局函数时,第一个const
的含义是返回值类型被修饰。
- 前缀形式返回改变后的对象,返回
*this
。 - 后缀形式返回改变之前的值,所以必须创建一个代表这个值的独立对象并返回它,是通过传值方式返回的。
7.3 成员函数重载二元运算符
重载赋值运算符
1 | Byte& operator=(const Byte& right) { // 只能用成员函数重载 |
1 | MinInt operator+(const MinInt& rv) const {//二元运算 + |
使用成员运算符的限制是左操作数必须是当前类的对象。
7.4 全局函数重载二元运算符
成员函数重载左操作数的类型是固定的。例如重载了+
,那么date+1
可以,1+date
不可以。==因为默认了左操作数的类型。但重载全局函数不存在该问题。==
希望运算符的两个操作数都能进行类型转换,则使用全局函数重载运算符。
1 | 重载operator>>的基本形式如下: |
1 | friend ostream& operator<<(ostream&os,const Date&d); |
重要实例代码
1 |
|
7.5返回值优化
- 创建一个临时的Integer对象并返回它
Integer temp(left.i + right.i);return temp;
- 临时对象语法
return Integer(left.i + right.i);
编译器直接把这个对象创建在外部返回值的存储单元中,所以只需要调用一次构造函数,不需要拷贝构造函数和析构函数的调用。因此,使用临时对象语法的效率非常高,这被称为返回值优化。
八、继承和派生
8.1访问权限
① 基类中的私有成员无论哪种继承方式在派生类 中都是不能直接访问的。
② 在公有继承方式下,基类中公有成员和保护成 员在派生类中仍然是公有成员和保护成员。
③ 在私有继承方式下,基类中公有成员和保护成 员在派生类中都为私有成员。
④ 在保护继承方式下,基类中公有成员和保护成 员在派生类中都为保护成员。
private成员:私有成员只能被定义它们的类的成员函数访问,其他任何地方都不能直接访问。这意味着私有成员对于类的外部是不可见的。
protected成员:保护成员也只能被定义它们的类的成员函数访问,但它们还可以被该类的派生类的成员函数访问。对于类的外部,保护成员和私有成员一样是不可见的。
8.2单继承和多继承
构造函数和析构函数不可以继承。
1 | // 基类:圆 |
多继承的二义性
- 第一种二义性:调用不同基类中的相同成员时可能 出现二义性。
==解决方法:在函数调用时指明基类。==
例:
1 | DateTime dateTime(2024, 4, 22, 14, 30, 59); |
- 第二种二义性:菱形继承。
解决方法:虚基类。
- 多重继承的继承函数书写格式
1 | class A1{ |
- 多重继承的同名函数调用
1 | cout << setiosflags(ios::fixed) << setprecision(2) << s.GetArea()<< endl;//Sphere类的 |
8.3虚基类
为最远的派生类提供唯一的基类成员,而不重复产生多 次拷贝。
1 | class B{ private: int b;}; |
派生类只能通过最底层的派生类访问虚基类的成员:在虚基类的派生中,派生类只能通过最底层的派生类来访问虚基类的成员,而不能通过其他派生类来访问。这是因为虚基类的成员只会被继承一次,而且只会在最底层的派生类中存在。
1 | class Vehicle{ |
派生类的构造函数承担着对基类中数据成员初始化和对派生类自身数据成员初始化的双重任务。
当创建一个派生类的对象时,它的构造函数会自动调用所有基类的默认构造函数(没有参数的构造函数)来初始化基类部分的数据。
1 | class Teacher_Officer:public Teacher,public Officer{ |
九、多态
多态性是指相同的动词作⽤到不同类型的对象上。
==C++实现的多态性:==
编译时(静态联编):
- 函数重载
- 运算符重载
- 模板
运行时(动态联编):
- 虚函数
9.1静态联编和动态联编
联编:⼀个源程序需要经过编译、连接,才能成为可执 ⾏代码。上述过程中需要将⼀个函数调⽤链接上 相应的函数代码,这⼀过程称为联编。
virtual
关键字的作⽤:指示C++编译 器对该函数的调⽤进⾏动态联编。
要使用动态联编,必须满足以下两个条件:
成员函数必须是虚函数(virtual function)。
必须通过指向基类的指针或引用来调用成员函数。
1 |
|
c.draw();
和r.draw();
是通过对象直接调用成员函数,这种情况下,编译器在编译时期就可以确定调用哪个函数,这是静态联编。然而,
shape1->draw();
和shape2->draw();
是通过指向基类Shape
的指针调用成员函数。这种情况下,编译器在编译时期无法确定shape1
和shape2
指向的具体类型,只能在运行时根据shape1
和shape2
实际指向的对象类型来确定调用哪个函数,这就是动态联编。
9.2 虚函数
访问对象的不同去调用不同的函数。只有当访问虚函数是通过基类指针s时才可获得运⾏时的多态性。
虚函数只能是成员函数
函数覆盖(override):在派⽣类中,虚函数被重新定义以实现不同的操作。 这种⽅式称为函数超越 (overriding),⼜称 为函数覆盖。
纯虚函数:纯虚函数是指在基类中声明但是没有定义的虚函数,⽽且设置函数值等于零。
1 | virtual float area( )=0; |
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
抽象类:抽象类:包含有纯虚函数的类称为抽象类。
1 | class shape{ |
9.3函数隐藏和函数重写
函数重写 (Function Overriding) | 函数隐藏 (Function Hiding) | |
---|---|---|
概念 | 派生类中新定义的函数版本覆盖基类中的同名虚函数,实现多态性。 | 派生类中定义与基类中同名的非虚函数,导致基类中的同名函数在派生类中不可见。 |
调用方式 | 根据指针或引用所指向的对象的实际类型来确定调用哪个版本的函数,实现运行时多态性。 | 当派生类定义了与基类同名的非虚函数时,调用该函数时将会调用派生类中的版本,而基类中的同名函数会被隐藏,无法直接调用。 |
关键字 | 基类中的函数必须声明为虚函数,并在派生类中使用 override 关键字进行重写。 |
派生类中的函数可以是虚函数,也可以是普通成员函数,不需要额外的关键字。 |
作用范围 | 发生在具有继承关系的类之间,通常用于实现基类与派生类之间的多态性。 | 同样也发生在具有继承关系的类之间,但是是通过在派生类中定义同名非虚函数而发生。 |
十、文件与输入输出控制
10.1 iostream
中的控制符
1 | cout<<hex<<num<<endl; |
10.2 iomanip
中的控制符
setfill('x')
从setfill
开始之后每个填充都会是'*'
,记得重置setbase(16)
设置输出的进制setw(10)
右对齐设置宽度为10,一次性,只可作用于一个输出setiosflags(ios::left/right);
设置左、右对齐setprecision(n)
设置输出数据的精确度为n,包括小数和整数部分。
分析:
1.
1 | cout << setfill('*') << setw(10) << "Hello" << endl; // 输出"Hello"并用'*'填充到10个字符宽 |
setw(15)
仅针对一个输入“hello world”
后设置的右对齐会覆盖左对齐。
2.
1 | double num=10.1; |
- 仅用
setprecision(5)
不能精确控制小数位数,通常连用fixed<<setprecision(5)
10.3文件操作
- 文件访问方式
std::ofstream
:用于文件输出。std::ifstream
:用于文件输入。std::fstream
:同时支持文件输入和输出。
1 | using namespace std; |
- 文件打开
1 | /*两种打开方式等价*/ |
- 文件关闭
1 | f1.close(); |
关闭文件操作包括把缓冲区数据完整地写入文件,添加文件结束标志,切断流对象和外部文件的连接
- 文件的读出写入
1 | f1<<20<<"hello"<<40;//在文件的开头插入 |
1 | char c; |
==必须掌握==
1 | /*写文件*/ |
完整用例
1 | int main() { |
十一、模板
11.1函数模板
1 |
|
11.2类模板
1 |
|
十二、异常处理
12.1 throw-try-catch
throw
语句用于抛出一个异常。这个异常可以是任何类型的值或对象。
1 | throw expression; |
try
块包含可能抛出异常的代码。catch
块用于捕获并处理异常。
1 | try { |
throw
语句会立即终止当前的代码块,并跳转到与之最近的catch
块。如果
throw
语句在一个try
块内部,那么它会跳转到这个try
块后面的catch
块。如果throw
语句不在任何try
块内部,或者没有找到匹配的catch
块,那么程序会调用std::terminate
函数,通常会导致程序终止。
catch
块的参数类型必须与throw
语句抛出的异常类型匹配。如果有多个catch
块,那么会选择第一个匹配的catch
块。
catch
块的参数是一个新的局部变量,它的值是throw
语句抛出的异常值的副本。如果异常是一个对象,那么这个对象会被复制。为了避免复制的开销,通常会抛出和捕获异常对象的引用。如果异常是一个类类型,那么可以使用基类来捕获所有的派生类异常。例如,
catch (const std::exception& e)
可以捕获所有派生自std::exception
的异常。可以使用
catch (...)
来捕获所有类型的异常,但是这样做无法获取到异常的具体信息。通常,catch (...)
会在最后一个catch
块,用于捕获其他catch
块没有捕获到的异常。在
catch
块中,应该避免再次抛出异常,除非你打算在更高的层次处理这个异常。如果在catch
块中抛出了新的异常,那么原来的异常就会丢失。异常应该用于处理真正的异常情况,而不是用于控制正常的程序流程。过度使用异常会使代码难以理解和维护,也会影响程序的性能。
十三、理论性表述梳理
面向对象程序设计特点包括封装性,继承性,多态性。
**构造函数和析构函数都不可以继承。**final说明的类不能被继承。友元函数不会被继承。
虚函数
- 只能是成员函数。
- 基类和派生类函数名和参数列表必须相同。
- 抽象类指的是包含纯虚函数的类,抽象类无法用于实例化对象。
- 为了实现运行时多态性,派生类应该使用公有继承。
构造析构顺序
- 当创建一个派生类的对象时,首先会调用基类的构造函数,然后是派生类的构造函数。
- 当销毁一个派生类的对象时,首先会调用派生类的析构函数,然后是基类的析构函数。
友元函数:
- 如果函数 fun()被说明为类 A 的友元,那么在 fun()中可以访问类 A 的私有成员
- 友元关系不能被继承
- 友元破坏了类的封装性
- 尽管友元函数可以访问类的私有和保护成员,但它仍然不是类的成员函数,因此它不能使用类的this指针
函数重载
- 函数重载是根据函数的参数列表来确定的
- 参数列表中参数的数量,类型和顺序
类
- 属性(Attribute):用于描述对象的静态特征。
- 方法(Method):用于描述对象的动态特征。
派生类
- 派生类不能继承基类的任何构造函数。派生类必须定义自己的构造函数,并在需要时显式调用基类的构造函数。
只能作为类的成员函数重载的运算符包括:
- 赋值运算符 (
=
)- 函数调用运算符 (
()
)- 下标运算符 (
[]
)- 成员访问运算符 (
->
)这些运算符在重载时必须定义为类的成员函数,因为它们需要直接访问类的内部结构或成员。
局部变量位于栈,全局变量和静态变量位于数据段(静态区)。
malloc
或new
申请的空间为堆区。模板运行时不检查数据类型,也不保证类型安全,相当于类型的宏替换。
类模板中的成员函数全是模板函数。
const int b; float* &c;
只能通过参数列表初始化。