C++面向对象(上)
# 一、类 & 对象
将过程和逻辑抽象出人更容易理解的抽象模块,模拟物种(类)和个体(对象)
# 1.1类 & 对象
C 语言面向过程编程
C++ 面向对象编程:在 C 语言的基础上增加了面向和对象设计,从功能代码转向人类更容易理解的一个个事物,
类: C++ 的核心特性,用于指定对象的形式,包含了【数据表示法】和【处理数据的方法】,被称为类的成员。
# 1.2类定义
类定义是以关键字 class 开头,后跟类的名称。
类只是告诉你数据和方法定义,以及如何执行,本身不具备数据存储能力。
class Person
{
public:
double length;
int age;
string name;
};
⚠️ 在没有修饰成员类型时,struct 的成员默认访问方式是 public , class 是的成员默认访问方式是 private
所以公开访问的成员,一般class 要假设public 修饰,而结构体公开的不需要,不设置就是默认公开
# 1.3对象定义
Person 人类是类,那么小明就是一个人(对象)
Person xiaoming; // 对象xiaoming
# 1.4访问数据成员
完整实例以及数据访问:
#include <iostream>
using namespace std;
class Person
{
public:
double height;
int age;
string name;
};
int main( )
{
Person per1; // 人对象1
// 对象特征设置
per1.height = 155.0;
per1.age = 16.0;
per1.name = "小明";
cout << "身高:" << per1.height <<endl;
return 0;
}
执行:
身高: 155.0
# 1.5类的成员函数
前面我们通过对象per1 获取 类成员 ”身高“,还有其他方法,就是通过 函数来获取,同时这个函数也是类的一个成员,我们称他为 成员函数
#include <iostream>
using namespace std;
class Person
{
public:
double height;
int age;
string name;
// 成员函数1(⚠️ 在类内同时也是 内联函数)(获取身高)
int getAge(void)
{
return age;
}
// 成员函数2(在类外定义)
void setAge(int age_new);
};
// 在类的外部使用【围解析运算符 :: 】定义成员函数,结构如下:
void Person::setAge(int age_new)
{
age = age_new;
}
// 上面我们实现了一个get方法和一个set方法,下面拿来使用
int main( )
{
Person per1; // 人对象1
// 对象特征设置
per1.setAge(16.0);
cout << "年龄:" << per1.getAge() <<endl;
return 0;
}
# 1.6访问修饰符
一个类可以有多个 public、protected 或 private 标记区域
# 公有(public)成员
✅ 这些成员都能被对象直接访问,前面例子都是,已经介绍过。
# 私有(private)成员
❌ 显然是不能被对象直接访问的成员,都是在定义时使用
❌ 同时子类也不能在定义中使用私有成员,完全是当前类型唯一使用
# 保护(protected)成员
❌ 不能被对象直接访问的成员,都是在定义时使用
✅ 子类(派生类)可以在定义中使用私有成员
示例:
#include <iostream>
using namespace std;
class Person
{
public:
// 公开成员
double height;
// 公开成员函数
int getAge(void)
void setAge(int age_new);
private:
int age; // 私有成员
protected:
string name; // 保护成员
};
// 公开成员函数操作私有成员
int Person::getAge(void)
{
return age;
}
void Person::setAge(int age_new)
{
age = age_new;
}
// 定义子类(派生类):学生👨🎓
class Student:Person
{
public:
// 公开成员函数,操作父类的保护成员
string getName(void)
void setName(string name_new);
}
// 子类公开成员函数操作 保护成员
string Student::getName(void)
{
return name;
}
void Student::setName(string name_new)
{
name = name_new;
}
// 上面我们实现了一个get方法和一个set方法,下面拿来使用
int main( )
{
Person per1; // 声明对象1
// ⚠️ 我们不能使用 .age 设置或者获取年龄,只能通过 公开的成员函数来操作
per1.setAge(16.0);
cout << "年龄:" << per1.getAge() <<endl;
Student stud1;
stud1.setName("小明");
cout << "学生姓名:" << per1.getName() <<endl;
return 0;
}
# 类构造函数 & 析构函数
对象创建调用函数(OC中叫alloc - init 、alloc - initWith xxx),和对象销毁时调用的函数(OC中叫 dealloc)
1.构造:
简单理解就是 对象创建时调用的函数(成员函数)叫 类构造函数(对象分配内存后立即调用的函数)
构造函数通常可以是多种写法,由程序员选择其中一种创建对象
函数方法名称同类名
分配内存
2.析构:
对象销毁时调用的函数,由系统自动调用,
析构函数通常只有一种固定写法
函数方法名称同类名,但是前面需要加 "~" 修饰
清理销毁对象的内存
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 构造函数
Line(double len); // 这是带参数的构造函数
~Line(); // 这是析构函数声明
private:
double length;
};
// 构造函数(成员函数)定义
Line::Line(void)
{
cout << "对象被创建" << endl;
}
// 带参数的构造函数定义,同时设置成员数据
Line::Line( double len)
{
cout << "对象被创建, length = " << len << endl;
length = len;
}
Line::~Line(void)
{
cout << "对象被销毁(对象释放时,系统自动调用此成员函数)" << endl;
}
// set方法
void Line::setLength( double len )
{
length = len;
}
// get 方法
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 或者
Line line(88.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
结果
对象被创建, length = 88
Length of line : 88
对象被销毁(对象释放时,系统自动调用此成员函数)
另外,当一个对象有多个成员时,构造函数,可以使用初始化列表来初始化字段:
class Point
{
public:
double X;
double Y;
Point(double x, double y); // 这是构造函数
};
// 方式1: 构造函数
Point::Point(double x, double y)
{
cout << "构造函数" << endl;
X = x;
Y = y;
}
// 方式2: 初始化列表方式的构造函数-(效果等同方式1)
Point::Point(double x, double y): X(x), Y(y)
{
cout << "构造函数-初始化列表方式" << endl;
}
# 类对象的拷贝
类对象的复制比简单的数据类型的复制要复杂一些,随着成员的复杂,克隆人的复制流程会更复杂。
1.简单的类对象复制:
// 程序的主函数
int main( )
{
int a = 88;
int b = a; // 普通的值拷贝,也叫赋值
Line line(88.0);
Line line2 = line; // 本质是调用【拷贝构造函数】,而非赋值
// 如果用户没有实现 拷贝构造函数,那么系统会自动生成对应的拷贝构造函数
return 0;
}
2.如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数,
下面实现一个带有指针成员的类构造函数和对象拷贝,完整示例如下:
#include <iostream>
using namespace std;
class Line
{
public:
int getLength( void );
Line( int len ); // 构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 构造函数
Line::Line(int len)
{
cout << "为指针分配内存" << endl;
ptr = new int;
*ptr = len;
}
// 拷贝构造函数(传入是同类型对象地址)
Line::Line(const Line &obj)
{
cout << "为指针拷贝内存地址." << endl;
ptr = new int;
*ptr = *obj.ptr; // copy
}
// 析构函数
Line::~Line(void)
{
cout << "内存释放" << endl;
delete ptr;
}
// get成员函数
int Line::getLength( void )
{
return *ptr;
}
// 程序的主函数
int main( )
{
Line line(88);
Line line2 = line;
return 0;
}
# 友元函数
声明:函数声明在某个类中,并使用关键字 friend 修饰
定义:函数的实现不属于某个类,是文件级别的
先看示例:
#include <iostream>
using namespace std;
class Box
{
public:
double height;
Box(double len, double wid, double hei); // 构造函数
friend void printVolume(Box box); // 友元函数:获取体积
private:
double width;
protected:
double length;
};
// 构造函数
Box::Box(double len, double wid, double hei): length(len), width(wid), height(hei)
{
cout << "构造函数-初始化列表方式" << endl;
}
// ✅ 友元函数: ⚠️ 这个不是Box成员函数,函数前面不需要 Box 限制
void printVolume(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "box 体积 : " << box.length * box.width * box.height <<endl;
}
// 程序的主函数
int main( )
{
Box box(3, 4, 5);
// 使用友元函数输出体积
printVolume( box );
return 0;
}
运行结果:
构造函数-初始化列表方式
box 体积 : 60
# 什么是函数
前面介绍了多种形式的函数,构造函数,析构函数,成员函数,友元函数等等
为了更好地理解后面要介绍的其他函数,这里开始介绍函数的基本概念
先看一段代码:
#include <iostream>
using namespace std;
// 程序的主函数
int main( )
{
int a = 3;
int b = 5;
// 求最大值并打印输出
int max1 = a > b ? a : b;
cout << a << "和" << b << "中最大值 : " << max1 <<endl;
int a2 = 30;
int b2 = 6;
// 求最大值并打印输出
int max2 = a2 > b2 ? a2 : b2;
cout << a2 << "和" << b2 << "中最大值 : " << max2 <<endl;
int a3 = 8;
int b3 = 50;
// 求最大值并打印输出
int max3 = a3 > b3 ? a3 : b3;
cout << a3 << "和" << b3 << "中最大值 : " << max3 <<endl;
// 假如后面还要执行成百上千次呢?
return 0;
}
打印结果:
3和5中最大值 : 5
30和6中最大值 : 30
8和50中最大值 : 50
有么有发现什么问题❓
相同的功能,重复的代码逻辑,假如需要执行一万次最大值,会发生什么:
1、你写到什么时候能写完
2、代码文件会很大很大
显然这都是不可取的,假如我们这样写:
#include <iostream>
using namespace std;
// ✅ 打印输出最大值
void p_max(int a, int b)
{
int max1 = a > b ? a : b;
cout << a << "和" << b << "中最大值 : " << max1 <<endl;
}
// 程序的主函数
int main( )
{
// 求最大值并打印输出
p_max(3, 5);
p_max(30, 6);
p_max(8, 50);
return 0;
}
这样就很清晰了,将重复的代码逻辑抽离,并重复使用一份代码,抽离的代码调用就是函数调用。
一万次只需要一次递归或者其它循环就能完成,代码量很少
(1)函数的由来
一个程序经常会通过多次执行相同或者相近功能的程序段来完成,在早期的程序设计中,这些重复的功能段必须通过重复书写代码来实现。这样,不仅会引起重复的劳动、增加程序的长度、造成代码的不一致,而更重要的是,大量重复的程序代码不利于程序的立即与理解。
于是人们将功能重复的程序段抽象出来形成一个独立的功能模块,并为它命名,程序中凡是用到此功能模块的地方就用他的名字代替,这样避免了重复设计的缺点。这种抽象出来的功能模块成为函数或者子程序。
(2)函数的执行过程
当调用一个函数时,整个调用过程分为三步进行,
第一步:函数调用 1、将函数调用语句下一条语句的地址保存到在栈中,以便函数调用完成后返回。(将函数放到栈空间中称为压栈)。
2、对实参表从后向前,一次计算出实参的值,并且将值压栈。
3、跳转到函数体处。
第二步:函数体执行
4、如果函数体中定义了变量,将变量压栈
5、将每一个形参以栈中对应的实参值取代,执行函数体的功能体。
6、将函数体中的变量、保存到栈中的实参值,依次从栈中取出,释放栈空间(出栈)。
第三步:返回
7、返回过程执行的是函数体中的return语句。其过程是从栈中取出刚开始调用函数时压入的地址,跳转到函数的下一条语句。当return语句不带有表达式时,按照保存的地址返回,当return语句带有表达式时,将计算出的return表达式的值保存起来,然后再返回。
⚠️ 这些压栈,出栈,跳转 等等一系列操作都是会额外增加耗时的,但是这些耗时相对于多行代码的执行来说可以忽略。相对于重复的代码劳动可以忽略,相对于大量重复代码导致文件成倍增加可以忽略...
# 内联函数
内联:函数内部直接关联代码块(不一定理解对,但是便于记忆)
外联:函数外部关联某些代码块
前面介绍 “最大值并打印输出” 重复代码的写法 就是函数内部直接关联,其实就是内联的本质。
当我们执行一段代码需要通过文件引用,或者调用其他函数时,就是外联。
**比如:**以前端代码为例
当css 样式代码直接写在 html中,可以理解为 内联
当css 样式代码通过css文件引入 html中,可以理解为 外联
下面修改,也就是函数 前添加 inline :
#include <iostream>
using namespace std;
// 打印输出最大值
// ⚠️ ✅ 函数前添加 inline 就可以表示内联函数
inline void p_max(int a, int b)
{
cout << a << "和" << b << "中最大值 : " << a > b ? a : b <<endl;
}
// 程序的主函数
int main( )
{
// 求最大值并打印输出
p_max(3, 5);
p_max(30, 6);
p_max(8, 50);
return 0;
}
那么这段代码再执行编译后,真实的代码逻辑类似这样的:
(实际上会进行进一步优化,这里写法为了方便比较)
#include <iostream>
using namespace std;
// 程序的主函数
int main( )
{
// 求最大值并打印输出
int a = 3;
int b = 5;
cout << a << "和" << b << "中最大值 : " << (a > b ? a : b) <<endl;
int a2 = 30;
int b2 = 6;
cout << a2 << "和" << b2 << "中最大值 : " << (a2 > b2 ? a2 : b2) <<endl;
int a3 = 8;
int b3 = 50;
cout << a3 << "和" << b3 << "中最大值 : " << (a3 > b3 ? a3 : b3) <<endl;
return 0;
}
很显然,这又回到了先前重复代码写法的方式
没错,本质就是这样,内联函数在编译时直接将代码块插入到需要的地方,哪里需要插入到哪里。
【⚠️ 类的成员函数也是内联函数,不需要显示使用 inline,我的理解是多个相同的类,创建不同的对象时,本质上都有一份对应的代码,显然这些代码都是重复的,与内联本质不谋而合(这样方便记忆)】
其他主要点:
1、普通函数需要 保存寄存器,压栈,出栈,链接,调用等等动作,如果一个函数的代码及其简单,本身已经小于一个函数调用产生的消耗,那么直接内嵌代码执行速度更快,即通过重复的代码牺牲空间来提高速度。(空间换时间)
2、当一个函数执行代码消耗远大于函数调用的开销,那么这个函数就不能成为内联函数,即使你 使用 inline修饰函数,编译时依然当成一个普通函数使用
3、当一个函数满足内联要求,但是通过递归,或者for循环等进行大量的重复操作,那么最终也不能成为内联函数
4、对内联函数进行任何修改,都需要重新编译函数的所有客户端,否则为编译的地方依然使用旧的逻辑
5、你的内联函数通常就是一两行简单的代码
6、同普通函数一样,除了声明,还要实现定义
# 指向对象指针: this
就是对象的成员函数中隐藏一个参数 this,指向当前对象的地址。(OC中叫 self,一个意思)
友元函数不是成员函数,没有this隐藏参数。
示例:this->Area()
#include <iostream>
using namespace std;
class Box
{
public:
// 构造函数定义:长,宽,高
Box(double l=2.0, double w=2.0, double h=2.0)
{
length = l;
width = w;
height = h;
}
double Area()
{
return length * width;
}
double Volume()
{
// ✅ 通过 this指针访问面积函数
return this->Area() * height;
}
private:
double length; //长
double width; //宽
double height; //高
};
int main(void)
{
Box Box1(13, 7, 44);
cout << "box 体积 : " << Box1.Volume() <<endl;
return 0;
}
结果
box 体积 : 4004
# 指向类指针
通过【*】声明一个指向类的指针,同时这个指针存储某个对象的内存地址。
通过这个指针访问类成员函数,本质上就是对象调用其成员函数一个道理。
修改上面代码示例如下:
#include <iostream>
using namespace std;
class Box
{
public:
// 构造函数定义:长,宽,高
Box(double l=2.0, double w=2.0, double h=2.0)
{
length = l;
width = w;
height = h;
}
double Area()
{
return length * width;
}
double Volume()
{
//通过 this指针访问面积函数
return this->Area() * height;
}
private:
double length; //长
double width; //宽
double height; //高
};
int main(void)
{
Box Box1(13, 7, 44);
cout << "box 体积 : " << Box1.Volume() <<endl;
Box *ptrBox; // ✅ 声明一个指向类的指针
// 指针保存类对象的地址
ptrBox = &Box1;
// 使用指针访问成员,通过->访问
cout << "指针访问 box 体积: " << ptrBox->Volume() << endl;
return 0;
}
# 静态成员static
几个要点:
1、静态意思就是非变化的值,相关值都必须是静态的
2、静态成员只有一份:比如一个类的静态成员,在多个不同的对象中都是共用一份的,不会随着对象的销毁而销毁(实际上静态成员内存和对象的内存不在一个区的,所以生命周期不一样)
3、类外部通过运算符 :: 来调用
示例:
#include <iostream>
using namespace std;
class Box
{
public:
static int objectCount; // 静态成员
// 构造函数定义:长,宽,高
Box(double l=1.0, double w=2.0, double h=3.0)
{
length = l;
width = w;
height = h;
}
// 静态成员函数
static int getCreatCount()
{
// return objectCount + length; 错误,不能在静态成员函数里使用变量,非静态的
return objectCount;
}
private:
double length; //长
double width; //宽
double height; //高
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
cout << "程序首次初始化Box对象前的对象数: " << Box::getCount() << endl; // 通过类名访问,而非对象访问
Box Box1(4.5, 6.7, 8.9); // 声明 box1
Box Box2(3.5, 4.0, 8.0); // 声明 box2
cout << "创建过对象之后,通过类获取创建过的对象总数:" << Box::getCount() << endl;
return 0;
}
# 二、继承
# 继承
继承应该是再熟悉不过的了,父亲立遗嘱让孩子继承他的财产和房子,
那么父亲就称为基类(父类)
而继承者称为派生类(子类)
而被继承的数据成员,和成员函数是(财产和房子)
面向对象程序设计中例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。
所示:
#include <iostream>
using namespace std;
// 基类(父类)
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类(子类)
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
cout << "面积: " << Rect.getArea() << endl;
return 0;
}
继承访问权限如下所示:
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes(可以继承) | no |
外部的类 | yes | no | no |
不能被继承的:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
# 多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。(可惜,这在OC中确是不允许的)
C++ 类可以从多个类继承成员,各个基类之间用逗号分隔
如下所示:
#include <iostream>
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类,同时继承 Shape 和 PaintCost
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}