奇闻轶事
T0fv404 2025-06-01 22:30
怎么办C++什么都看不懂了QAQ,我不要看,我不要看口牙!~(大声)
2025-06-01 23:02 Nan0in
铸币吧课一节不上自己也不学,早干嘛了
前言
首先HDU C++面向对象编程。。。怎么说呢老师该讲的确实都讲了,但是我不喜欢课程的节奏就自己去看C++ Primer Plus了,但是这确实是个大任务因为书太厚了
我们期末考是笔试,课程评分分为
期末70 平时30
期末考40分选择,20道题一题2分
10分判断
程序填空10分
程序阅读20分
程序设计题20分
考点其实也不多,有时间看我C++ Primer Plus读书笔记博客
那就开始概括吧,部分参考了学长的资料
概括
绪论
面向对象与面向过程:
- 类和对象区别
- 类的基本概念
- 封装
函数
- 参数什么时候传入引用
&
,什么时候不需要(理解引用)
- 内联函数是什么?如何实现?作用是?
- 默认参数值的函数以及默认参数值的说明顺序
- 函数重载,什么函数可以重载,如何实现函数重载
类与对象
- 抽象 封装 继承 多态四大件
- 类的定义
- 继承的三种方式
- 构造函数,默认构造函数,拷贝构造函数以及拷贝时的赋值(什么时候会调用拷贝构造函数),什么时候编译器会自动为你生成一个默认构造函数
- 析构函数
- 字符串类
- 深拷贝和浅拷贝(你要理解指针的原理和使用)
- 组合类的特点和构造函数
数组 指针
- 数组名->指针(理解)
- 常量指针和指针常量
- 指针数组
- this指针
- 函数指针
- 回调函数
- new delete使用动态
数据共享与保护
- 生存期与可见性
- 静态成员函数、数据成员以及他们的特点和使用场景,如何调用静态成员函数
- 友元类和友元函数
- 保护,你要理解const常对象,常成员和常引用
- 常成员函数的声明形式
- 顶层const和底层cosnt含义和区别
继承与派生
- 单继承多继承的概念
- 派生类语法和派生类使用和生成过程
- 改造基类成员(同名的时候怎么办?)
- 类型兼容规则
- 多继承构造函数
- 派生与基类对象重名(ppt里有例子)
- 区分虚继承、虚基类和虚函数
多态
- 运算符重载(只有已存在支持运算符重载)
- 定义为成员函数的运算符重载和定义为非成员函数的运算符重载
- 友元
- 整形+复数 运算实现的复现 前置++后置++的重载
- 虚函数,构造函数不设计虚函数而析构函数鼓励设置成虚函数的原因
- 纯虚函数
- 抽象基类
重点考点 5.3 5.4 指针 指针关键考察:bool char int long int,表2-1 对象指针(类名对象指针名) this指针用法 new和delete不考 类型兼容规则实例的代码非常关键,“基类的构造函数不被继承这一页也是重要考点” 派生类的析构函数,同名成员举例部分,虚基类作用7-8
实现shape类(必考点,派生类调用,代码补全)几乎可以重载全部运算符(你觉得这句话为什么要加几乎 “.” “::“等不会重载)重载看实型+复数的例题,单目运算符重载方式 虚函数,虚函数必须是非静态的成员函数(典型代码,跟之前虚基类之间选一个进行考)抽象类的特点和理解,构造函数不虚,析构可虚,参数传递考小题
详情
面向过程与面向对象程序设计
面向过程程序设计
自顶而下,分而治之
程序结构
- 按功能划分模块,形成树状结构
- 各模块间关系尽可能简单,功能上相对独立
面向对象程序设计
- 将客观事物看作具有属性和行为的对象
- 抽象出同一类别对象的共同属性和行为将其封装作为类
- 通过类的继承、多态实现代码复用
概念:
类与对象——模具与实际件 类是人,对象是独特个体
- 封装
将对象的属性和行为结合成独立的系统单元,意思是学会尽可能隐藏对象内部信息,只保留有限对外接口发生联系与交互
函数
内联函数
inline,编译时在调用处用函数体进行替换,节省参数传递、控制转移等开销
- 内联函数体内无循环和switch
- 内联函数声明在第一次被调用之前
- 对照宏:
#define ADD(A,B) A+B
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream>
using namespace std;
const double PI=...
inline double calArea(double radius){
return PI*radius*radius;
}
int main(){
double r=3.0;
double area = calArea(r);
cout<<area<endl;
return 0;
}
|
带默认参数函数
有默认参数的形参必须在形参列表后,也就是说默认参数右面不能有无默认值参数,调用时实参与形参结合从左向右
1
2
3
|
int add(int x,int y=5,int z=6); //✔对
int add(int x=4,int y=5,int z); //xx错
int add(int x=1,int y int z=6); //xx错
|
如果一个函数有原型声明,且原型声明在定义之前,则默认参数必须在函数原型声明中给出;而如果只有函数的定义或函数定义在前,默认参数在函数定义中给出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//1 原型声明在前
int add(int x=5,int y=6);
int main(){
add();
}
//这里不能再指定默认值了
int add(int x,int y){
return x+y;
}
int add(int x=5,int y=6){
//直接定义
return x+y;
}
int main(){
add();
}
|
函数重载
将功能相似的函数以相同函数名声明形成重载,便于使用
1
2
3
4
5
6
7
|
int add(int x,int y);
//类型不同
float add(float x,float y);
//形参个数不同
int add(int x,int y,int z);
|
注意形参必须存在不同,或个数
或类型
而名字和返回值是不可行的
——根据函数有无const
决定是否重载,而编译程序根据实参和形参类型、个数会自动最佳匹配来选择调用哪一个函数
析构函数不可重载
参数传递
- 值传递——传递参数值,单向
- 引用传递——双向传递 使用
&
引用传递声明时候必须同时对它进行初始化,使其指向一个已存在的对象
1
2
3
4
|
int i,j;
int &ri=i;
j=10;
ri=j; //i=j
|
一个引用初始化后不能改为指向其他对象,可作形参
类与对象
类
具有相同属性和行为的一组对象的集合,为属于该类的全部对象提供抽统一抽象描述,内部包含属性和行为
数据的封装、继承与派生都是利用类实现的
利用类更易于编写大型复杂程序,模块化程度比C中使用函数更高
构造函数
没有返回值
- 作用:在对象被创建的时候使用特定值构造对象
- 对象创建时自动调用
- 程序中未声明则系统生出默认构造函数,参数为空
- 类型:可以是内联函数、重载函数、带默认参数值函数(其实就是大部分你学过的函数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 时钟实例
#include <iostream>
using namespace std;
class Clock {
public:
Clock(int h, int m, int s);
void setTime(int h, int m, int s);
void showTime() { printf("%d %d %d\n", hour, minute, second); }
private:
int hour;
int minute;
int second;
};
Clock::Clock(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
int main() {
Clock c1(0, 0, 0);//调用有参数构造函数,无参数的就直接Clock c2,系统会生成默认构造函数;
c1.showTime();
return 0;
}
|
复制构造函数
特殊的构造函数,形参为本类的对象引用。作用上会将已存在的对象初始化同类型的新对象
当我们没有明确定义一个复制构造函数,编译器会生成一个
格式如下
1
2
3
4
5
6
7
8
9
10
|
class 类名
{
public:
类名(形参表)
类名(类名 &对象名); //复制构造函数
...
};
类名::类名(类名 & 对象名){
函数体//具体实现
}
|
具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
class Point
{
public:
Point(int xx=0,yy=0){
x=xx;y=yy; //构造函数
}
Point(Point &p);//复制构造函数
int getX(){return x;}
int getY(){return y;}
private:
int x,y;
}
|
被调用的三种情况:
- 定义一个对象, 以本类其他对象作为初始值
- 函数的形参是类的对象,调用函数时,使用实参对象初始化形参对象,发生复制构造
- 返回值是类的对象,函数执行完成返回主调函数,使用return语句中对象初始化一个临时无名对象(可见运算符重载
)
下面看一个覆盖了三种情况的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void fun1(Point p){
cout<<p.getX()<<endl;
}
Point fun2(){
Point a(1,2);
return a;
}
void main(){
Point a(4,5); //第一个对象a
Point b=a; //情况一,用A初始化B
cout<<b.getX()<<endl;
fun1(b);//情况二,对象B作为fun1实参,调用复制构造函数
b=fun2();//情况三,函数返回值是类对象,函数返回调用复制构造函数
cout<<b.getX()<<endl;
}
|
析构函数
- 完成对象被删除前的一些清理工作
- 在对象的生存期结束的时刻系统调用并释放对象所属空间
- 程序中未声明析构函数,编译器会自动生成一个隐含的析构函数
动态内存分配
new 类型名T(初始化参数列表)
- 用于程序执行期间,申请用于存放T类型对象内存空间,成功会抛出T类型指针
delete 指针p
以上二者必须配对使用
1
2
3
4
5
6
7
8
|
Point* ptr1 = new Point; //可看看ptr1值
delete ptr1;
int *x = new int; //x不确定
int *x = new int(); //x=0,如果删除这个的话这个空间会被赋值一个奇怪的数,可以自己看看
Point* ptr=new Point[2]; // 创建对象数组
delete[] ptr; //删除整个对象数组
|
浅复制和深复制
浅复制
- 只复制指针,相当于源对象实际上与复制对象公用一个内存
数据成员带有指针时,使用浅复制可能出现问题(比如内存泄漏,然后我们就可以伪造数据,或许就能pwn了嘿嘿)
比如这个图中的World这部分的内存就没有指针会去指向它了,有因为没有被销毁,成为了孤儿内存
深复制
一并复制对象空间和资源,源对象与复制对象互相独立,占不同内存。
一般需要自行写 复制构造函数和赋值函数
注意在赋值的时候检测是否 自我赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#include <bits/stdc++.h>
using namespace std;
class String {
public:
String(const char *cstr = 0);
String(const String &str);
String &operator=(const String &str);
~String();
private:
char *m_data;
};
String::String(const char *cstr) {
if (cstr) {
m_data = new char[strlen(cstr) + 1];
strcpy(m_data, cstr);
} else {
m_data = new char[1];
*m_data = '\0';
}
}
String::~String() { delete[] m_data; }
String::String(const String &str) {
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
String &String::operator=(const String &str) {
if (this == &str)
return *this; // 如果已经有自我赋值就返回
delete[] m_data; // 释放原有内存
m_data = new char[strlen(str.m_data) + 1]; // 分配新内存
strcpy(m_data, str.m_data); // 复制内容
return *this;
}
int main() {
String s1;
String s2("hello");// 构造
String s3(s1); //复制构造
s3 = s2;
return 0;
}
|
总结一下:深复制会一并复制对象空间和资源,而源对象与复制对象互相独立占不同内存,需要自己写复制构造函数和赋值函数
类的组合
类中成员数据是另一个类的对象
在已有抽象的基础上实现复杂抽象,也就是组合类
比如在Point类上定义Line类和三角形类
组合类的构造函数
第一步先调用内嵌对象的构造函数,调用顺序按照对象在组合类定义中出现顺序来,根据初始化列表判断使用构造函数还是拷贝构造函数。
如果一个内嵌对象没有默认构造函数,在组合类的初始化列表中要给对象成员也初始化,对于继承的构造函数同理
第二步然后执行本类构造函数的函数体
析构函数调用顺序相反(你就当作先进后出这种)
语法格式:类名:::类名(形参列表):内嵌对象1(形参表),形参对象2(形参表)
实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
#include <cmath>
#include <iostream>
using namespace std;
class Point {
public:
Point(int xx = 0, int yy = 0) {
x = xx;
y = yy; // 构造函数
}
Point(Point &p) {
x = p.x;
y = p.y;
cout << "copy constructor has been called for (" << x << "," << y << ")"
<< endl;
} // 复制构造函数
int getX() { return x; }
int getY() { return y; }
private:
int x, y;
};
class Line {
public:
Line(Point xp1, Point xp2);
Line(Line &l);
double getLen() { return len; }
private:
Point p1, p2;
double len;
};
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) { // 组合类构造函数,内部再次调用复制构造函数进行p1 p2赋值
cout << "calling constructor for Line" << endl;
double x = static_cast<double>(p1.getX() - p2.getX());
double y = static_cast<double>(p1.getY() - p2.getY());
len = sqrt(x * x + y * y);
};
Line::Line(Line &l) : p1(l.p1), p2(l.p2) { // 组合类复制构造函数
cout << "calling copy constructor for Line" << endl;
len = l.len;
};
int main() {
Point myp1(1, 1), myp2(4, 5);
Line line(myp1, myp2);
puts("This is LINE~~~~~~~~~~~~~~~~~~~~");
Line line2(line); // 复制构造函数建立新对象
cout << "the length of the line1 is " << line.getLen() << endl;
cout << "the length of the line2 is " << line2.getLen() << endl;
return 0;
}
|
抽象
对具体对象(即问题)进行概括,抽出该类对象的公共性质并加以描述的过程。
以时钟为例
- 数据抽象:描述某类对象的属性或状态(时分秒)
- 代码抽象:描述某类对象的共有的行为特征或具有的功能。(比如设定时间、显示时间的功能)
抽象通过类的声明具体实现
封装
将抽象的数据成员、代码成员相结合,视为一个整体
目的上增强了安全性,简化了编程,使用者也不必了解具体实现细节,通过接口和特定访问权限来使用类的成员
{}
用于类声明中实现封装
1
2
3
4
5
6
7
8
|
class Clock
{
public:
void setTime(int newH,int newM,int newS);
void showTime();
private:
int hour,minute,second;
}
|
继承
C++中支持层次分类的机制,允许程序员在保持原有类特性的基础上进行更具体的说明 ,见下文继承与派生
多态
见下文多态与多态性
数据共享与保护
文件作用域
这个很好区分的看实例,局部作用域在块中声明,作用域自声明处起,限定在块中
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include<iostream>
using namespace std;
int i; //全局
void main(){
i=5; //全局i赋值
{
int i; // 局部
i = 7;
cout<< "i = "<<i<<endl;//7
}
cout<<" i = " <<i<<endl;//5
}
|
- 可见性
- 标识符声明在先,引用在后
- 某个标识符外层声明,内层没有同一标识符声明,该标识符内层可见
- 对于两个嵌套作用域,如果在内层作用域声明与外层作用域同名标识符,外层作用域标识符在内层不可见(暂时覆盖)
- 就近原则
- 对象生存期
- 对象从产生到结束的时间
- 对象生存期内,对象保持它的值直到被更新
- 函数内部声明静态生存期对象-
static
(不会被重置)
- 动态生存期
- 块作用域中声明的,没有用static修饰的对象是动态生存期对象
- 开始于程序执行到声明点,结束于命名该标识符的作用域结束处
静态数据成员相关
- static声明
- 被类所有对象共享,静态生存期
- 类外定义和初始化,
::
指明所属类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
static int count; // static member variable
public:
Point(int x = 0, int y = 0) : x(x), y(y) {
// 构造函数中进行++,因此所有对象共享该唯一一个count
count++;
}
Point(Point &p) {
x = p.x;
y = p.y;
count++;
}
~Point() { count--; }
int getX() { return x; }
int getY() { return y; }
void showCount() { cout << " Object count = " << count << endl; }
};
int Point::count = 0; // 初始化,使用类名限定
int main() {
Point a(4, 5);
cout << "Point A: " << a.getX() << "," << a.getY() << endl;
a.showCount();
Point b(a);
cout << "Point B: " << b.getX() << "," << b.getY() << endl;
b.showCount();
return 0;
}
|
静态成员函数只能访问静态数据成员、静态成员函数
如
1
2
3
4
5
6
7
|
static void showCount(){
cout<<"Object count = " <<count<<endl;
}
//main中,可以通过
//1.Point::showCount();
//2.a.showCount();
|
类的友元
(非常匪夷所思)
- c++提供的破坏数据封装和数据隐藏的机制(尽量不使用和减少使用以保证数据封装和隐藏)
- 使用友元函数和友元类
友元函数
^fc3201
- 用
friend
修饰说明的非成员函数,在函数体中可以通过对象名访问private和protected成员(进行了破坏)
- 作用:增加了灵活性,是程序员更好在封装和快速性做合理选择
- 访问对象中成员必须通过对象名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <cmath>
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
int getX() { return x; }
int getY() { return y; }
friend float dist(Point &a, Point &b);
private:
int x, y;
};
float dist(Point &a, Point &b) {
double x = a.x - b.x;
double y = a.y - b.y;
return float(sqrt(x * x + y * y));
}
int main() {
Point p1(1, 1), p2(4, 5);
cout << "Distance between p1 and p2 is: " << dist(p1, p2) << endl;
return 0;
}
|
dist这里设置为了非类内相关的普通成员函数,由此设置friend
可以访问到类内private成员
友元类
- 一个类为另一个类的友元,此类中所有成员都可以访问对方类的私有成员
- 用
friend
将友元类名在另一个类中修饰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#include <iostream> //>
using namespace std;
class A {
friend class B;
public:
void display() { cout << x << endl; }
private:
int x;
};
class B {
public:
void set(int i);
void display();
private:
A a;
};
void B::set(int i) {
a.x = i; // Accessing private member of class A
}
void B::display() {
a.display(); // Calling display method of class A
}
int main() {
B a;
a.set(1);
a.display();
return 0;
}
|
最后打印a.x为B访问的A成员的x,也就是1
友元关系是单向的,我们在A中设置B类是A类的友元,B类的成员函数可以访问A类的私有和保护数据,但是A类的成员函数不能访问B类的私有、保护数据
共享数据保护
- ⭐在一些情况下,对于既需要共享、又需要改变的数据我们就设置为常类型
const
(这东西你会看到很多次的)
- 对于不改变对象状态的成员函数都应该声明为常函数
常对象
必须进行初始化,不会被更新
const 类名 对象名
1
2
3
4
5
6
7
8
9
|
class A{
public:
A(int i,int j) {x=i;y=j;}
...
private:
int x,y;
};
const A a(3,4); //常对象a,不会被更新
|
常成员、常引用和常成员函数
常成员
用const进行修饰的类成员
常数据成员要么直接初始化,要么在构造函数里初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
using namespace std;
class A {
public:
A(int i) : a(i) {}
void print();
private:
const int a; // const int a=10就可初始化
static const int b; // 静态常数据成员
};
const int A::b = 10;
void A::print() { cout << a << ":" << b << endl; }
int main() {
A a1(100), a2(0);
a1.print();
a2.print();
return 0;
}
|
常引用
const 类型说明符 &引用名
- 常引用的被引用对象不能被更新
- 常引用做形参就不会 意外发生对实参的修改
对上文友元函数
进行修改
friend float dist(const Point &p1,const Point &p2);
这样引入的参数必然是安全常态的
常成员函数
类型说明符 函数名(参数表) const;
- 使用关键字说明的函数
- 不更新对象的数据成员
- const关键字被用于参与对重载函数的区分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
using namespace std;
class R {
public:
R(int r1, int r2) : r1(r1), r2(r2) {};
void print();
void print() const;
private:
int r1, r2;
};
void R::print() { cout << r1 << ":" << r2 << endl; }
void R::print() const { cout << r1 << ";" << r2 << endl; }
int main() {
R a(5, 4);
a.print(); // Calls the non-const version
const R b(20, 52);
b.print(); // Calls the const version
return 0;
}
|
通过常对象只能调用它的常成员函数,所以是要一一配对的
指针常量和常量指针
指针常量——指针本身的值不能被改变
1
2
3
4
|
int a;
int* const p2=&a;
int b;
p2=&b;//❌p2为常量指针不能修改
|
常量指针——不能通过指针修改所指对象的值 ,但指针本身可以修改指向别的对象
1
2
3
4
5
|
int a;
const int *p1=&a; //p1是指向常量的指针
int b;
p1=&b;//✔
*p1=1;//❌
|
1
2
3
4
5
6
7
|
int* const p;//p所指地址不能被修改
int const *p;//p所指位置可以修改,但是不能通过*p=...来修改所指向地址的值,但是可以指向另一个地址
const int *p; //同第二个
int a=2;
int const &b=a;//b不能改,只能a=2实现修改
const int* f();//返回的指针所指向的值不能通过该指针修改
|
指针⭐
赋值
- “地址”存放与指针类型相符合的值
- 指针变量所赋的值必须是地址常量或变量(不可以是普通整数)。但可以赋值为整数0来表示空指针
- 指针类型与其所指向变量类型相同,任何一个指针本身数据值都是unsigned long int(地址是64位的,8字节)
- void类型可声明。该指针会被赋予任何类型对象地址,使用时必须指定类型
1
2
3
4
|
int num=0;
void *prt;
prt=#
printf("%d\n",*((int*)ptr));
|
运算
指针p+n表示指针指向位置的前后n个数据的地址 ->p[n]=*(p+n)
指针作为函数参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream>
using namespace std;
void split(float x, int *intpart, float *fracpart) {
*intpart = int(x);
*fracpart = x - *intpart;
}
int main() {
float x, f;
int n;
cin >> x;
split(x, &n, &f);
cout << "Integer part: " << n << endl << "fractional part: " << f << endl;
return 0;
}
|
对象指针
声明:类名 *对象指针名
1
2
3
|
Point a(5,10);
Point *prt;
ptr=&a;
|
访问的话
1
2
3
4
5
6
|
void main(){
Point a(4,5);
Point *p1=&a;
cout<<p1->getX()<<endl;//指针访问
cout<<a.getX()<<endl; //对象名访问
}
|
this指针
- 隐含于每⼀个类的成员函数中的特殊指针
- 明确地指出了成员函数当前所操作的数据所属的对象。
- 当通过⼀个对象调⽤成员函数时,系统先将该对象的地址赋给this指针,然后调⽤成员函数,成员函数对对象的数据成员进⾏操作时,就隐含使⽤了this指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
int getX();
int getY();
private:
int x, y;
};
int Point::getX() {
return this->x; // return x;
}
int main() {
Point p(3, 4);
cout << p.getX() << endl;
return 0;
}
|
函数指针
type (*fp)(tyep1,type2);
- 示例:
int (*fp)(int int)
- 可将其他匹配函数赋予fp, 则fp可调⽤此函数
两个案例:
1
2
3
4
5
6
7
8
9
10
|
#include <iostream>
using namespace std;
int (*fp)(int, int);
int add(int a, int b) { return a + b; }
int main() {
int x = 2, y = 3;
fp = add; // 这⾥&add也可以
cout << (*fp)(x, y) << endl;
return 0;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <iostream>
using namespace std;
int (*fp)(int, int);
int fun1(int x, int y, int (*fp)(int, int)) { return (*fp)(x, y); }
int max(int x, int y) { return x > y ? x : y; }
int min(int x, int y) { return x < y ? x : y; }
int add(int x, int y) { return x + y; }
int sub(int x, int y) { return x - y; }
int main() {
int x = 4, y = 1;
cout << "max(x, y) = " << fun1(x, y, max) << endl; // 这⾥max和&max都可以
cout << "min(x, y) = " << fun1(x, y, min) << endl;
cout << "add(x, y) = " << fun1(x, y, add) << endl;
cout << "sub(x, y) = " << fun1(x, y, sub) << endl;
return 0;
}
|
继承与派生
- 保持已有类的特性⽽构造新类的过程称为继承
- 在已有类的基础上新增⾃⼰的特性⽽产⽣新类的过程称为派⽣,新类为 派生类
- 被继承的已有类称为基类(或⽗类)
- 继承目的——代码重用
- 派生目的——新的问题出现,原有程序⽆法解决或不能完全解决时,需要对原有程序进⾏改造
单继承和多继承
派生类只从单个基类派生——单继承(树状结构)
派生类有两个或以上的基类——多继承
派生类的声明和生成
1
2
3
4
|
class 派生类名:继承方式1 基类名1,继承方式2 基类名2,...
{
成员声明;
}
|
例子:
1
2
3
4
5
6
|
class Derived:public Base1,private Base2
{
public:
Derived();
~Derived();
}
|
- 生成过程:
- 吸收基类成员,派⽣类就包含了它的全部基类中除构造函数和析构函数之外的所有成员。
- 改造基类成员,如果派⽣类声明了⼀个和某基类成员同名的新成员(如果是成员函数,则参数表也要相同,参数不同的情况属于重载),派⽣类的新成员就覆盖了外层同名成员
- 添加新的成员,派⽣类新成员的加⼊是继承与派⽣机制的核⼼,是保证派⽣类在功能上有所发展 ^87738a
- 访问控制:
- 不同继承方式有三种,区别体现在访问权限上——派生类成员对基类成员的访问权限和通过派生类对象对基类成员的访问权限
不同的继承方式
- 公有继承public
- 私有继承private(默认)
- 保护继承protected
公有继承
- 基类public和protected成员的访问属性在派生类中保持不变,基类private成员不可直接访问
- 派生类成员函数可以直接访问基类中的public和protected成员,但不能直接访问private成员
- 通过派生类对象只能访问基类public成员
类成员访问属性
- public:任意访问(类内,类外)
- private:只能被本类成员函数访问、类的友元除外(即类内可访问、类外不行)
- protected:本类及派生类的成员函数访问(类内可访问、派生类可访问、类外不行)
private>protected>public
日常使用——public(想想也是用的最多的)
protect和private区别要牢记,见图
派生类的构造函数
基类构造函数不继承,要自己写
写的时候只需对本类中新增成员初始化,对继承得到的基类成员初始化,
自动调用基类构造函数完成如果基类没有默认构造函数,那么一定要调用基类的带参数的构造函数
派生类的构造函数要给基类的构造函数 传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#include <iostream>
using namespace std;
class B {
public:
B(int i); //没有默认构造函数,只有一个带参数的构造函数
~B();
void print() const;
private:
int b;
};
B::B(int i) { b = i; }
B::~B() {}
void B::print() const { cout << "b = " << b << endl; }
class C : public B {
public:
C(int i);
C(int i, int j);
~C();
void print() const;
private:
int c;
};
C::C(int i) : B(i) { c = 0; }
C::C(int i, int j) : B(i), c(j) {} //传递参数给基类
C::~C() {}
void C::print() const {
B::print(); // 重名函数调用,输出下面赋的b
cout << "c = " << c << endl;
}
int main() {
C obj(5, 6);
obj.print();
return 0;
}
|
多继承时构造函数调用顺序
- 对于继承的基类成员,先调用基类构造函数,调用顺序按照它们 被继承时 声明的顺序(从左向右)
- 对本类成员初始化列表中的基类成员和对象成员进行初始化,初始化顺序按照它们在类中声明顺序。对象成员初始化自动调用对象所属类的构造函数完成
- 执行派生类的构造函数中内容
派生类的析构函数
- 析构函数不被继承,自行声明
- 声明方法与一般类的析构函数相同
- 不需要显式调用基类析构函数,系统会自动调用
- 顺序上先执行派生类的,再调用基类的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <iostream>
using namespace std;
class Base1 {
public:
Base1(int i) { cout << "constructing Base1 " << i << endl; }
~Base1() { cout << "Destructing Base1" << endl; }
};
class Base2 {
public:
Base2(int j) { cout << "constructing Base2 " << j << endl; }
~Base2() { cout << "Destructing Base2" << endl; }
};
class Base3 {
public:
Base3() { cout << "constructing Base3 * " << endl; }
~Base3() { cout << "Destructing Base3" << endl; }
};
class Derived : public Base2, public Base1, public Base3 {
public:
Derived(int a, int b, int c, int d)
: Base1(a), member2(d), member1(c), Base2(b) {}
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1, 2, 3, 4);
return 0;
}
|
析构和构造顺序相反
派生类和基类成员出现同名
- 若无特别限定,则通过派生类对象使用的是派生类中的同名成员
- 如要通过派生类对象访问基类中被隐藏的同名成员,应该使用基类名和作用域操作符(::)来限定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <iostream>
using namespace std;
class Base1 {
public:
int var;
void fun() { cout << "Memeber of Base1" << endl; }
};
class Base2 {
public:
int var;
void fun() { cout << "Member of Base2" << endl; }
};
class Derived : public Base1, public Base2 {
public:
int var;
void fun() { cout << "Member of Derived" << endl; }
};
int main() {
Derived d;
Derived *p = &d;
d.var = 1; //对象名.成员名
d.fun();//访问Derived对象 第一个fun,默认derived
d.Base1::var = 2; //作用域操作符标识
d.Base1::fun();//访问Base1基类成员 第二个fun
p->Base2::var = 3;//作用域操作符标识进行访问
p->Base2::fun();//访问Base2基类成员 第三个fun
return 0;
}
|
基类成员同名
当派⽣类从多个基类派⽣,⽽这些基类⼜从同⼀个基类派⽣,则在访问此共同基类中的成员时,将产⽣⼆义性,那么应该怎么办呢? 请看下面虚基类
举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <iostream>
using namespace std;
class Base0 {
public:
int var0;
void fun0() { cout << "member of Base0" << endl; }
};
class Base1 : public Base0 { // 第一个派生基类
public:
int var1;
void set1() { var0 = 1; }
};
class Base2 : public Base0 { // 第二个派生基类
public:
int var2;
void set2() { var0 = 2; }
};
class Derived : public Base1, public Base2 { // 派生类
public:
int var;
void set3() { Base1::var0 = 0; }
void fun() { cout << "Member of Derived" << endl; }
};
int main() {
Derived x;
x.set1();
x.set2();
cout << x.Base1::var0 << endl;
cout << x.Base2::var0 << endl;
cout << x.var0 << endl; // 访问Base1的var0,但是这里会出现二义性错误,base1和base2都设置过var0,因此编译器无法理解是哪个var0了
return 0;
}
|
因此也会导致编译错误
也可以看这个图,更详细
虚基类
- 引入:用于有共同基类的场合
- 声明:以
virtual
修饰说明,例如:class B1:virtual public B
- 作用:
- 解决多继承可能发生的对同一基类继承多次导致的二义性问题
- 为最远派生类提供唯一基类成员而不重复多次拷贝
我们对源代码进行少量修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <iostream>
using namespace std;
class Base0 {
public:
int var0;
void fun0() { cout << "member of Base0=" <<var0<< endl; }
};
class Base1 : virtual public Base0 { // 第一个派生基类
public:
int var1;
void set1() { var0 = 1; }
};
class Base2 : virtual public Base0 { // 第二个派生基类
public:
int var2;
void set2() { var0 = 2; }
};
class Derived : public Base1, public Base2 { // 派生类
public:
int var;
void fun() { cout << "Member of Derived" << endl; }
};
int main() {
Derived x;
// x.set1();
// x.set2();
// cout << x.Base1::var0 << endl;
// cout << x.Base2::var0 << endl;
// cout << x.var0 << endl; // 访问Base1的var0
x.var0 = 2; // 直接访问虚基类的数据成员
x.fun0(); // 直接访问虚基类的函数成员
return 0;
}
|
没有二义性!因为现在无论从 Base1
还是 Base2
继承的路径最终都共享这一个虚基类的成员。这时候只有唯一的var0存在,被两个派生同时指向
虚基类及其派⽣类构造函数
- 建立对象时所指的最远类——最远派生类
- 虚基类成员由最远派生类的构造函数调用了虚基类的构造函数进行初始化
- 建立对象的时候,只有最远派生类的构造函数调用虚基类的
构造函数调用函数
先看这张图
- 同一层中,先调用虚基类,再调用非虚基类的(BCD相比:先C D再B)
- 同一层中有多个虚基类,按照它们在继承方式中的声明次序调用(CD相比,则按声明顺序在E中声明)
- 基类由非虚基类派生,则先调用基类的,再按派生类中执行顺序调用(如AD相比,先A后D)
多态与多态性
多态
即操作接口具有表现多种形态的能力,可以根据不同操作环境采用不同处理方式
- 分类:按时机编译时多态和运行时多态分为两种,另有分类
- 重载多态:函数重载,运算符重载
- 强制多态:强制类型转换
- 包含多态:不同类中的同名成员函数
- 参数多态:函数模板、类模板
- 目的:实现行为标示统一而减少程序中标识符的个数
- 具体实现方式:重载函数和虚函数
多态性:用一个名字定义不同的函数,执行不同而相似的操作从而可以使用相同的调用方式来调用不同功能的同名函数
以下是多种不同的多态声明方式
1
2
3
4
|
//强制多态
double Add(int x,double y){
return x+y;
}
|
1
2
3
4
5
6
7
8
9
10
11
|
//不同类中的同名成员函数
class Base{
public:
void Show();
}
class Derived:public Base //Derived公有继承于Base
{
public:
void Show();
}
|
1
2
3
4
5
6
7
8
9
|
//module of functions参数多态
template<class T>
T Add(T x,T y)
int a=0lint b=1;int c;
int d=3.0;int e=4.5;int f;
c=Add(a,b);
f=Add(d,e);
|
关于多态的实现
- 编译时的多态(功能先绑定)
- 编译过程中就确定同名操作的具体操作对象(函数重载
、强制多态、参数多态)
- 运行时的多态(功能后绑定)
- 运行过程中动态确定同名操作的具体操作对象(包含多态——虚函数)
运算符重载
^a8d5d6
- C++几乎可重载所有运算符
. :: ?:
等不可
- 重载后运算符优先级结合性不变
- 分为类成员函数重载和非成员函数重载
重要代码之复数计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <bits/stdc++.h>
using namespace std;
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
Complex operator+(const Complex &other) const {
return Complex(real + other.real, imag + other.imag);//会创建一个临时无名对象作为返回值
}
Complex operator-(const Complex &other) const {
return Complex(real - other.real, imag - other.imag);
}
void display() const { cout << real << " + " << imag << "i" << endl; }
};
int main() {
Complex c1(3, 4), c2(1, 2);
Complex sum = c1 + c2;
Complex diff = c1 - c2;
cout << "a+b = ";
sum.display();
cout << "a-b = ";
diff.display();
return 0;
}
|
重载为成员函数
函数类型 operator 运算符(形参)
注意参数个数为原操作个数-1(存在特例双目运算符)
双目运算符
- 如果要重载B为类成员函数,使之能够实现
a1 B a2
其中a1为A类对象,则B应被重载为A类的成员函数,形参类型为a2所属类型
- 重载后,
a1 B a2
= a1.operator B(a2)
前置单目运算符
- 如果要重载U为类成员函数,使之能够实现
U a
,其中a为A类对象,则U应该被重载为A类的成员函数,注意无形参
- 经重载后,表达式U a相当于a.operator U()
例如:重载一个复数类的"-"
后置单目运算符++ –
- 若要重载++或–为类成员函数,使之能够实现表达式
a++
a--
。其中a为A类对象,则++或–应被重载为A类的成员函数,有一个int类型形参
- 重载后,a++相当于a.operator ++(0)
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
#include <iostream>
using namespace std;
class Clock {
public:
Clock(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}
// 构造函数
void showTime() const;
Clock operator+(const Clock &x) const;
Clock &operator++();
Clock operator++(int);
void set(int h, int m, int s) {
hour = h;
minute = m;
second = s;
}
private:
int hour, minute, second;
};
void Clock::showTime() const {
cout << hour << ":" << minute << ":" << second << endl;
}
Clock &Clock::operator++() { //前置运算符++重载
second++;
if (second >= 60) {
second -= 60;
minute++;
if (minute >= 60) {
minute -= 60;
hour = (hour + 1) % 24;
}
}
return *this;
}
Clock Clock::operator++(int) {
Clock temp = *this; // 后置单目++重载
++(*this); // 调用前置++,实现了返回old的同时进行了++
return temp;
}
Clock Clock::operator+(const Clock &x) const {
Clock temp;
temp.hour = hour + x.hour;
temp.minute = minute + x.minute;
temp.second = second + x.second;
while (temp.second >= 60) {
temp.second -= 60;
temp.minute++;
}
while (temp.minute >= 60) {
temp.minute -= 60;
temp.hour++;
}
return temp;
}
int main() {
Clock c1, c2;
c1.set(1, 3, 2);
c2.set(1, 6, 3);
c1++;
++c2;
c1.showTime();
c2.showTime();
Clock c3 = c1 + c2;
c3.showTime();
return 0;
}
|
重载为非成员函数
- 函数形参代表依次从左向右排列的各操作数
- ⭐后置单目运算符++和–的重载函数,形参列表要增加一个int,但不需要形参名
- 在运算符的重载函数中需要操作某类对象私有成员,可以将函数声明为该类的友元 (
friend Clock operator+(const Clock &c1, const Clock &c2);
)这样就可以访问内部private成员(当然不是Clock没问题,这里方便才这么写)
还是用复数当实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
#include <bits/stdc++.h>
using namespace std;
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
friend Complex operator+(const Complex &c1, const Complex &c2);
friend Complex operator+(const double &c1, const Complex &c2);
friend Complex operator*(const Complex &c1, const Complex &c2);
friend Complex operator++(Complex &c1);
friend Complex operator++(Complex &c1, int); // 后置
void display() const { printf("%lf %lf\n", real, imag); }
};
Complex operator+(const Complex &c1, const Complex &c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
};
Complex operator+(const double &c1, const Complex &c2) {
return Complex(c1 + c2.real, c2.imag);
}
Complex operator*(const Complex &c1, const Complex &c2) {
return Complex(c1.real * c2.real - c1.imag * c2.imag,
c1.real * c2.imag + c1.imag * c2.real);
}
Complex operator++(Complex &c1) {
c1.real++;
return c1;
}
Complex operator++(Complex &c1, int) {
Complex temp = c1; // 保存当前值
c1.real++;
return temp;
}
int main() {
Complex c1(5, 4), c2(2, 10), c3;
cout << "c1 = ";
c1.display();
cout << "c2 = ";
c2.display();
c3 = c1 + c2;
cout << "c3 = c1 + c2 = ";
c3.display();
c3 = c1 * c2;
cout << "c3 = c1 * c2 = ";
c3.display();
c3++;
c3.display();
return 0;
}
|
虚函数
- 用
virtual
关键字说明的函数
- 虚函数是实现运行时多态的基础
- c++的虚函数是动态绑定的函数,并且虚函数必须是 非静态的函数,经过派生后,实现运行过程中的多态
- 而派生类中虚函数的机制使得可以对基类中的成员函数进行覆盖(也就是重定义)
声明:
1
2
3
|
virtual 函数类型 函数名 (形参表){
函数体
}
|
实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <iostream>
using namespace std;
class Base1 { // 基类
public:
virtual void
show() const; // 虚函数,可以试着把virtual去掉看看没有虚函数是什么效果
};
void Base1::show() const { cout << "Base1::show()" << endl; }
class Base2 : public Base1 {
public:
void
show() const; // 想要覆盖基类的虚函数,参数和返回值以及是否为const必须一致
};
void Base2::show() const { cout << "Base2::show()" << endl; }
class Derived : public Base2 {
public:
void show() const; // 这是一个普通成员函数,不是虚函数
};
void Derived::show() const { cout << "Derived::show()" << endl; }
void fun(Base1 *ptr) { // 参数为指向基类对象的指针
ptr->show(); // 调用的是Base1的虚函数
}
int main() {
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
base1.show(); // 调用Base1的虚函数
derived.show(); // 调用Derived的普通成员函数
return 0;
}
|
如果去掉了virtual是这样的
所有通过基类指针的 ptr->show()
将在编译时绑定为 Base1::show()
,即不会发生多态。而如果声明virtual则会因为覆盖动态绑定成员实现调用重写(override)后的函数,而不是 基类 的函数
不过c++ 11后就可以通过
1
2
3
|
class Derived : public Base {
void show() const override; // 编译器会检查是否正确重写
};
|
彻底检查是否重写成功
虚析构函数
- 需求原因:可能通过基类指针删除派⽣类对象;
- 如果打算允许基类指针调用对象的析构函数(例如使用delete函数),就需要让基类的析构函数成为虚函数,否则执行delete结果不确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <iostream>
using namespace std;
class Base {
public:
~Base(); // vitrual ~Base();如果定义成虚函数就可以调用对应的析构函数
// 否则调用fun函数时调用基类的析构函数
};
Base::~Base() { cout << "Base destructor" << endl; }
class Derived : public Base {
public:
Derived();
~Derived();
private:
int *p;
};
Derived::Derived() { p = new int(0); }
Derived::~Derived() {
cout << "Derived destructor" << endl;
delete p;
}
void fun(Base *b) { delete b; }
int main() {
Base *b = new Derived();
fun(b);
return 0;
}
|
若不将析构函数声明为虚函数则
若改为virtual则
想象你通过电话(基类指针)打给一个人(对象),你以为对面接的是总机(Base),结果其实是某个部门(Derived)。
只有把析构函数设成虚函数时,程序才知道去问总机:“你代表的是哪个部门?”,才能正确转接给那个部门来处理资源销毁。
纯虚函数
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为
virtual 函数类型 函数名(参数表)=0
相当于每个汉堡店都要有汉堡,但汉堡只有一个定义而没有具体实现,不同的店要做的就是将派生的汉堡进行具体的实现和定义一些操作
- 带有纯虚函数的类被称为抽象类
作用:
- 抽象类为抽象和设计的⽬的⽽声明,将有关的数据和⾏为组织在⼀个继承层次结构中,保证派⽣类具有要求的行为。
- 对于暂时⽆法实现的函数,可以声明为纯虚函数,留给派⽣类去实现。
注意
- 不能声明抽象类对象
- 构造函数不能是虚函数,而析构函数可以是虚函数
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
#include <cmath>
#include <iostream>
using namespace std;
class Shape {
protected:
int n;
double *data;
public:
Shape(int num) : n(num) { data = new double[n]; }
virtual ~Shape() { delete[] data; }
virtual double getArea() const = 0;
virtual double getPerim() const = 0;
};
class Rect : public Shape {
public:
Rect(double length, double width) : Shape(2) {
data[0] = length;
data[1] = width;
}
double getArea() const { return data[0] * data[1]; }
double getPerim() const { return 2 * (data[0] + data[1]); }
};
class Circle : public Shape {
public:
Circle(double radius) : Shape(1) { data[0] = radius; }
double getArea() const { return M_PI * data[0] * data[0]; }
double getPerim() const { return M_PI * data[0] * 2; }
};
class Tri : public Shape {
public:
Tri(double a, double b, double c) : Shape(3) {
data[0] = a;
data[1] = b;
data[2] = c;
}
double getArea() const {
double p = getPerim() / 2;
return sqrt(p * (p - data[0]) * (p - data[1]) * (p - data[2])); // 海伦公式
}
double getPerim() const { return data[0] + data[1] + data[2]; }
};
void fun(Shape *ptr) {
cout << ptr->getArea() << " " << ptr->getPerim() << endl;
}
int main() {
Rect rec(3, 4);
fun(&rec);
Circle cir(3);
fun(&cir);
Tri tri(3, 4, 5);
fun(&tri);
return 0;
}
|