C++高级语法 (8)
面向对象
什么是面向对象?
你怎么理解面向对象和面向过程?
之前老师上课说的时候,面向对象不是大多数学生想的多个class多个继承就等于面向对象,如果有人认为C++的面向对象就是在C的基础上多个一个class,只能说明水平不够对面向对象的理解不够。
对面向对象这一个概念应该有更深的理解,理解层次不同可以帮助学习的层次不同,也能够理解面向对象中很多东西存在的理由,以及他们的功能是做什么的。
面向对象的究极目标:代码复用(可扩展性本质也是代码复用)
面向对象的三大特性:封装(模块化)、继承、多态
封装、继承:实现代码复用,继承就是在一个类的基础上继续扩展,一个类看成一个模块
多态:包含静态多态(函数重载)和动态多态(子类重写父类虚函数,也只能是虚函数),一般只动态多态
1)背景:封装使代码模块化,模块之间会存在耦合(模块A依赖模块B)
2)解决方法:动态多态就是接触模块之间的耦合,让模块A不依赖一个具体的模块,而是依赖一个抽象的模块,这个抽象模块叫做抽象类,或者接口
3)怎么实现这个想法/功能呢?
虚函数,虚函数指针,虚函数表
虚函数的内容是C++中的重点,之前被一个C++大佬考察了基本功,问了个虚函数的问题,直接懵了,回答不上来
面向过程和面向对象区别?
- 面向过程的思路:什么事都自己做;分析解决问题所需的步骤,用函数把这些步骤依次实现。
- 面向对象的思路:什么事都指挥对象去做;面向对象的做法,其实就是按照“把复杂问题化简为单个的小问题”一般性工作思路,将程序要解决的问题切分为相对独立的实体,已达到理清其中关系明确任务边界的目的。
“如果上帝是程序员,他怎么创造世界上的所有动物。”理解这个问题就理解了面向对像。
C++中如何创建一个对象
在VS中可以快速创建一个类
在一个类中应当具备哪些基本的功能,都应该清楚的,要求自己能够很熟练的写出来一个类,并且没有什么大问题
下面的代码很简单,自己看看就知道了,但是一个类可不能只有这么一点的东西,为了让类的功能更加的强大,我们涉及到操作符重载的内容
#pragma once
class Complex
{
public:
Complex(); //默认构造函数
Complex(double real, double image); // 构造函数
Complex(const Complex &com); //拷贝赋值函数,这里要传入引用,因为会效率更高
void SetVal(double real,double image); //设置complex的参数
void SetImage(double image);
void SetReal(double real);
double GetImage() const; //设置函数
double GetReal() const;
~Complex();
private:
double _real;
double _image;
};
#include "Complex.h"
#include <iostream>
Complex::Complex() {
std::cout << "这是默认的构造函数" << std::endl;
}
Complex::Complex(double real,double image) {
std::cout << "这是传参的构造函数" << std::endl;
this->_image = image;
this->_real = real;
}
Complex::Complex(const Complex& com) {
this->_real = com._real;
this->_image = com._image;
}
void Complex::SetVal(double real, double image) {
this->_real = real;
this->_image = image;
}
void Complex::SetImage(double image) {
this->_image = image;
}
void Complex::SetReal(double real) {
this->_real = real;
}
double Complex::GetImage() const {
return _image;
}
double Complex::GetReal() const {
return _real;
}
Complex::~Complex() {
std::cout << "这是对象的析构函数" << std::endl;
}
运算符进行重载
操作符的重载让类的更像是一种内置的数据类型,他会拥有更强的实用性质:
你需要能够熟练的编写,前++,后++,+=,-=等操作符的重载,这属于C++程序员的基本功
1.概念
运算符的重载,实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该运算符时就调用此函数来行使运算符功能。这个函数叫做运算符重载函数(常为类的成员函数)。
用函数的方式实现了(+ - * / []数组 && || 逻辑 等)运算符的重载。根据需求决定重载那些运算符,用到的时候再百度案例即可。
2.运算符重载的两种方法
第一种类内重载:
//对 = 类内重载
Complex operator+(const Complex &com) {
Complex tmp;
tmp._image = com._image + this->_image;
tmp._real = com._image + this->_real;
return tmp;
}
//类内声明
Complex operator+(const Complex &com);
//类内声明,类外定义的 = 运算符重载
Complex Complex::operator+(const Complex &com) {
Complex tmp;
tmp._image = com._image + this->_image;
tmp._real = com._image + this->_real;
return tmp;
}
第二种为类外重载
//对 = 类外重载,声明函数为友元函数
friend Complex operator+(const Complex& com, const Complex& com2);
//类外 = 号的操作符重载
Complex operator+(const Complex& com, const Complex& com2) {
Complex tmp;
tmp._image = com._image + com2._image;
tmp._real = com._image + com2._real;
return tmp;
}
什么是临时对象?如何优化拷贝构造中出现的临时对象?
参考博主https://blog.csdn.net/m0_46606290/article/details/120070571的内容
在C++中很容易就写出一些代码,这些代码的特点就是偷偷的给你产生了一些临时对象,导致临时对象会调用拷贝构造函数,赋值运算符,析构函数,假如该对象还有继承的话,也会调用父类的拷贝构造函数,赋值运算赋函数等。这些临时对象所调用的函数,都是不必要的开销,也就是说,我本意不想你给我调用这些函数的,但你编译器却给我偷偷的调用了,就是由于我程序员写代码产生临时对象而产生的。
产生临时对象的原因主要有三种:
1.以值的方式给函数传参
运行下面的代码,发现多了一次拷贝构造函数说明除了我们原本的构造对象以外,还是产生了额外的对象,这个说明了在程序中出现了临时对象,临时对象的出现会降低效率,因此如何避免临时对象,是对程序的优化。
为什么会出现临时对象呢?
由于 fun成员函数里面的形参是Person p
,这样会导致在调用这个fun函数时候,会传递过去的是实参的复制品,临时对象,并不是外面main函数的实参,这里可以在fun函数里修改一样形参就可以发现,外面的实参没发生改变。
# include<iostream>
using namespace std;
class Person {
public:
Person() {
std::cout<< "默认构造函数!" << std::endl;
}
Person(int a)
{
m_age = a;
cout << "有参构造函数!" << endl;
}
Person(const Person& p)
{
m_age = p.m_age;
cout << "拷贝构造函数!" << endl;
}
~Person()
{
cout << "析构函数!" << endl;
}
int fun(Person p) //普通的成员函数,注意参数是以值的方式调用的
{
p.m_age = 20; //这里修改对外界没有印象
return p.m_age;
}
int m_age;
};
int main()
{
Person p(10);//初始化
p.fun(p);
return 0;
}
如何优化?
只要把值传递的方式修改为引用传递的方式即可。这样既不会调用拷贝构造函数,也不会调用多一次临时对象的析构函数。减少额外不必要的开销。
所以我们在函数形参设计时候,能够用引用就用引用的方式,因为这样可以减少对象的复制操作,减少而外的开销。
同时:这里应该要判断一下this是不是等于自己,如果传入的对象是自己的话应该也要做一个判断, 减少程序的开销,提高程序运行的效率是C++的开发目标。
int fun(Person &p)
{
if (this != &p) {
p.m_age = 999;
return p.m_age;
}
else {
return p.m_age;
}
}
2. 类型转换成临时对象 / 隐式类型转换保证函数调用成功
这种方式就是并且把类型转化前的对象当作了形参传递给构造函数,生成临时对象临时对象结束后就会调用析构函数。
类还是以上的类,只是main函数中执行的代码不同了
int main()
{
Person p;
p = 100;
return 0;
}
为什么会出现这样子的原因?
其实是由于 p = 1000;这句引起的,这里p的类型为 Person,而 1000为 int 类型,很明显类型不一致。
编译器其实偷偷的进行了类型转换,如何转换呢?编译器就是将1000传入了构造函数中
如果传入的Complex就不会出现这样子的情况
如何解决这样的问题?
赋值语句的对象如果不是相同的数据类型,会因为强制数据类型的转换,出现问题
只要把单参数构造函数的赋值语句,改为初始化语句就行。
那什么是赋值语句和初始化语句呢?
两者的区别就是
一个是创建对象同时赋值对象,也就是说创建时候就马上初始化,这就是初始化;
一个是创建对象时候不赋值对象,而是等对象创建好,过后使用再赋值对象,这就是赋值语句;
3. 函数返回对象时候
在函数返回对象时候,会创建一个临时对象接收这个对象;从而调用了拷贝构造函数,和析构函数。
当你调用函数,没有接收返回值时候,就会调用析构函数,因为都没有人接收返回值了,自然而然析构了。当你调用时候,有接收返回值时候,这个时候,并不会多调用一次析构函数,而是直接把临时对象返回值,给了接受返回值的变量来接收。
关于这部分的内容不是特别的好理解,可以看一下代码
转载上面博主的代码(但是我用他的代码运行,只有两次构造和两次析构)
如何解决这样子的问题?
两种解决办法
当我们在接收函数返回的对象时候,可以用右值引用接收,因为该函数返回值是一个临时变量,用一个右值引用接收它,使得它的生命周期得以延续,这样就少调用一次析构函数的开销。(当然普通的对象接收也是可以)
当我们在设计函数里的return 语句中,不是返回创建好的对象,而是返回我们临时创建的对象,即使用return类类型(形参); 这个时候,就可以直接避免 return 对象;返回时候又要调用多一次构造函数。
这两种行为就可以避免了构造函数和析构函数的产生。
//把这个代码修改:
Person test(Person & p)
{
Person p1; //这里会调用无参构造函数和结束的一次析构函数
p1.m_age = p.m_age;
return p1; //这里会多调用一次临时拷贝和析构函数
}
//修改为:
Person test(Person &p)
{
return Person(p.m_age);//直接返回临时对象,可以减少
}
//比如这个样子的代码,直接return返回,但其实底层的实现我并不是很了解
Complex Complex::operator+ (const Complex& c) const
{
//Complex tmp;
//tmp._real = _real + x._real;
//tmp._image = _image + x._image;
//return tmp;
return Complex(_real + c._real, _image + c._image);
}
请你对前置++和后置++进行重载,并且说出他们之间的不同之处
其实自己看代码就可以知道他们的不同地方,从底层的代码去理解的话,暂时水平不够
可以很明显的看出来,前++的效率更高
重点:为区别前置和后置运算符,C++编译器要求,需要在后置运算符重载函数中加参数“int”,这个类型在此除了以示区别之外并不代表任何实际含义;如果不加,编译器无法区分是前置++,还是后置++,导致报错。
Complex& Complex::operator++ () // 前置++
{
_real++;
_image++;
return *this;
}
//后置++的很多种不同写法,可以很明显的感觉到效率不同
Complex& operator++(int) {
Complex tmp;
tmp._real = this->_real;
tmp._image = this->_image;
this->_real++;
this->_image++;
return tmp;
}
Complex Complex::operator++ (int) {
Complex tmp(*this);
this->_real++;
this->_image++;
return tmp;
}
Complex Complex::operator++ (int) {
return Complex(_real++, _image++);
}
同理可以写出来前置–和后置–的区别
输入输出(IO)的重载
这一部分的内容其实还可以联系到IO缓冲的内容
要理解内容是怎么流动的,是存放在哪里的
//如果不包含using namespace std;
//就需要在ostream和istream前面加上std::
ostream& operator<<(ostream& os, const Complex &x)
{
os << "real value is " << x._real << " image value is " << x._image;
return os;
}
istream& operator >> (istream& is, Complex &x)
{
is >> x._real >> x._image;
return is;
}
可以检验,代码没有问题嘀!
IO操作
基础概述
看看PPT的内容吧
IO缓存
小例子
#include <iostream>
using namespace std;
int main() {
int a;
int index = 0;
while (cin >> a) {
cout << "The number is:" << a << endl;
index++;
if (index == 5) {
break;
}
}
char ch;
cin >> ch;
cout << "The char is:" << ch << endl;
return 0;
}
这段程序运行的时候会出现一点小问题,如下图所示,我其实想要的是先输入while循环内的a然后再去输入ch的内容,但是因为多输入的一个6,程序直接将IO缓存中的6直接放在了ch中
如何解决这个问题呢?
那就是对缓冲区中的内容进行清空处理即可
cin.ignore(1024, ‘\n’),通常把第一个参数设置得足够大,这样实际上是为了只有第二个参数 ‘\n’ 起作用,所以这一句就是把回车(包括回车)之前的所以字符从输入缓冲流中清除出去。
如果默认不给参数的话,默认参数为cin.ignore(1, EOF),即把EOF前的1个字符清掉,没有遇到EOF就清掉一个字符然后结束。如果没有
//在这里对哦cin中的缓存内容进行清空处理
cin.ignore(1024,'\n');
char ch;
cin >> ch;
cout << "The char is:" << ch << endl;
//这个表示是清空当前缓冲区中的所有内容
cin.ignore(numeric_limits<std::streamsize>::max(), '\n');
文件操作内容
利用文件打开的时候要按照下面的步骤进行,确保文件的打开是是顺利的
文件的打开方式
//这是C++中open的定义,可以看出来默认是用in和out读写方式打开的,所以文件如果没有的话,open就会打开失败,他不会自动去创建一个文件
void open(const char* _Filename, ios_base::openmode _Mode = ios_base::in | ios_base::out,
int _Prot = ios_base::_Default_open_prot) {
// _Prot is an extension
if (_Filebuffer.open(_Filename, _Mode, _Prot)) {
_Myios::clear();
} else {
_Myios::setstate(ios_base::failbit);
}
}
因此要用app(追加)的方式打开文件
#include <iostream>
#include <fstream>
using namespace std;
int main() {
int a;
int index = 0;
fstream fout;
//表示用追加的方式在文件的末尾写内容
fout.open("textBuffer.txt",ios::app);
//这里判断文件是否成功打开,同样也可以使用fout.fail()函数来进行判断
if (!fout) {
cout << "The open file is failed" << endl;
}
while (cin >> a) {
fout << "The number is:" << a << endl;
index++;
if (index == 5) {
break;
}
}
cin.ignore(numeric_limits<std::streamsize>::max(), '\n');
char ch;
cin >> ch;
fout << "The char is:" << ch << endl;
fout.close();
return 0;
}
头文件重复包含
在工程中经常会出现头文件重复包含的问题,在这里有两种方式如何积极而这个问题
条件编译
在这里可以看出来,如果宏定义是重复的那么就会出现这样子的情况,必须要保证么个宏定义的名称都是不一样
解决方法就是修改宏的名称,让他尽可能是唯一的
微软编译器检查
#pragma once其实非常的方便,也很简单,缺点就是只可以在window上的VS中来使用
浅拷贝和深拷贝(重点)
运行这段代码会出现错误,复制以下代码在VS中,思考为什么还会出现问题?
#include <iostream>
using namespace std;
class Person {
public:
Person() { /*默认构造函数*/
cout << "Person默认构造桉树的调用" << endl;
}
Person(int age,int height) { /*有参构造函数*/
m_age = age;
m_height = new int(height); //在堆区开辟内存
cout << "Person有参构造函数的调用" << endl;
}
~Person() {
if (m_height != NULL) {
delete m_height;
m_height = NULL;
}
cout << "Person析构函数的调用" << endl;
}
int m_age; //年龄
int* m_height; //体重
};
void test01(void) {
Person p1(18,160);
cout << "p1的年龄是" << p1.m_age << "体重是" << *p1.m_height << endl;
Person p2(p1);
cout << "p2的年龄是" << p2.m_age << "体重是" << *p2.m_height << endl;
}
int main(void)
{
test01();
system("pause");
return 0;
}
出现问题的原因是:对同一块内存区域进行了多次的释放操作,因为int* m_height
是一个指针,在text中P1和P2的指针都是指向了同一块的内存区域,对这同一块内存区域进行重复的释放,当然会报错。
由于栈区的规则是先进后出,当执行完拷贝构造函数的时候,就会执行p2的析构函数,导致释放堆区开辟的数据。因此当执行p1的析构函数时就会导致内存释放2次,程序崩溃。这部分的内容可以回顾C++中的内存四区
在对含有指针成员的对象进行拷贝时,必须自己定义拷贝构造函数,达到深拷贝的目的,才能不对内存重复释放。
修改代码,设置拷贝构造函数(深拷贝),再次运行就不会出现问题
Person (const Person& p) {
m_age = p.m_age;
m_height = new int(*p.m_height);
if (m_height != NULL) {
cout << "这是Person的拷贝构造函数" << endl;
}
else {
exit(-1);
}
}
移动构造函数
(10条消息) C++ 移动构造函数详解_吾爱技术圈的博客-CSDN博客
这个讲的很好,直接看这个算了
String类的声明
#pragma once
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
String(String&& other); // 移动构造函数
~String(void); // 析构函数
String& operator= (const String& other); // 赋值函数
String& operator=(String&& rhs)noexcept; // 移动赋值运算符
friend ostream& operator<<(ostream& os, const String &c); // cout输出
private:
char *m_data; // 用于保存字符串
};
String类的实现
类的实现一定要考虑到代码的健壮性!
// String 的普通构造函数
String::String(const char *str)
{
if (str == NULL)
{
m_data = new char[1];
if (m_data != NULL)
{
*m_data = '\0';
}
else
{
exit(-1);
}
}
else
{
int len = strlen(str);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, str);
}
else
{
exit(-1);
}
}
}
// 拷贝构造函数
String::String(const String &other)
{
int len = strlen(other.m_data);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, other.m_data);
}
else
{
exit(-1);
}
}
// 移动构造函数
String::String(String&& other)
{
if (other.m_data != NULL)
{
// 资源让渡
m_data = other.m_data;
other.m_data = NULL;
}
}
// 赋值函数
String& String::operator= (const String &other)
{
if (this == &other)
{
return *this;
}
// 释放原有的内容
delete[ ] m_data;
// 重新分配资源并赋值
int len = strlen(other.m_data);
m_data = new char[len + 1];
if (m_data != NULL)
{
strcpy(m_data, other.m_data);
}
else
{
exit(-1);
}
return *this;
}
// 移动赋值运算符
String& String::operator=(String&& rhs)noexcept
{
if(this != &rhs)
{
delete[] m_data;
m_data = rhs.m_data;
rhs.m_data = NULL;
}
return *this;
}
// String 的析构函数
String::~String()
{
if (m_data != NULL)
{
delete[] m_data;
}
}
ostream& operator<<(ostream& os, const String &c)
{
os << c.m_data;
return os;
}
// String 的析构函数,涉及一个析构函数应该要判断他的内容是否为NULL,这样可以避免内存的重复释放,如果不对成员判断是否为空的话,可能会出现堆内存的重复释放情况
String::~String()
{
if (m_data != NULL)
{
delete[] m_data;
}
}
虚函数和子类继承
#include <iostream>
using namespace std;
class Shape
{
public:
virtual double Area() const = 0;//在这里定义为纯虚函数,不能够创建这个类的实例对象
virtual void Show() = 0;
void Display()
{
cout << Area() << endl;
}
};
class Tri :public Shape
{
public:
Tri(double _len, double _high): _len(_len),_high(_high){}
double Area() const{//重写虚函数必须保证与父类是完全相同的,否则都不会被重写
return _len * _high * (0.5);
}
void Show() {
cout << "This is Tri" << endl;
}
private:
double _len;
double _high;
};
class Qur :public Shape
{
public:
Qur(double _len) : _len(_len){}
double Area() const {//重写虚函数必须保证与父类是完全相同的,否则都不会被重写
return _len * _len;
}
void Show() {
cout << "This is Qur" << endl;
}
private:
double _len;
};
int main() {
Tri t1(12.0,6.4);//如果构造函数对私有成员的赋值是在函数内部的话,这边就会出现无法解析的外部命令
Qur q1(10.5);
Shape* shape[2];
shape[0] = &t1;
shape[1] = &q1;
for (int i = 0; i < 2; i++) {
cout << shape[i]->Area() << endl;
shape[i]->Show();
}
return 0;
}
虚函数是非常重要的内容,能否回答上虚函数的问题直接关系到面试的最终结果
关于虚函数的内容涉及到下面的四个知识内容:
- 虚函数
虚函数也分为普通虚函数和纯虚函数,纯虚函数要求子类必须重写父类纯虚函数,而普通虚函数不要求必须重写
对于一个普通的类而言,就算里面什么东西都没有,也一定会占用一个字节的空间
class A{};
int main(){
A a;
cout << sizeof(a) << endl;//1
return 0;
}
下面的图片同理,普通函数不占用类的内存空间,但是他是属于这个类的内容,但是不占用这个类的内存空间
接下来引入虚函数,会发现内存的空间大小变成了四字节
我是在X86下是4,X64下就是8这个和电脑是32位的还是64位有关系
class Base {
virtual void A() {};
};
int main() {
Base a;
cout << sizeof(a) << endl;
return 0;
}
和这边的伪代码相似,相当于在Base类中插入的一个vptr的虚函数指针
可以复杂一点看,如果类中的虚函数这样子的话,怎么去判断类中的内存布局?
- 虚函数表和虚函数表指针
首先如果类中有虚函数,那么类中一定会有一个虚函数指针,这个虚函数表指针指向了虚函数表,虚函数表中存放的是虚函数的地址
什么时候才是多态?
这张图是继承中体现的多态性和虚函数的内容
面向对象的总结PPT