C++高级语法(8)

本文介绍了C++中的面向对象编程,包括面向对象的概念、与面向过程的区别,以及如何创建对象。重点讨论了运算符重载,临时对象的产生和优化,以及拷贝构造中的深拷贝问题。此外,还提到了虚函数在多态性中的作用,以及文件操作、输入输出流的相关内容。
摘要由CSDN通过智能技术生成

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;
}

虚函数是非常重要的内容,能否回答上虚函数的问题直接关系到面试的最终结果

关于虚函数的内容涉及到下面的四个知识内容:

在这里插入图片描述

  1. 虚函数

虚函数也分为普通虚函数和纯虚函数,纯虚函数要求子类必须重写父类纯虚函数,而普通虚函数不要求必须重写

对于一个普通的类而言,就算里面什么东西都没有,也一定会占用一个字节的空间

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的虚函数指针

在这里插入图片描述

可以复杂一点看,如果类中的虚函数这样子的话,怎么去判断类中的内存布局?

在这里插入图片描述

  1. 虚函数表和虚函数表指针

首先如果类中有虚函数,那么类中一定会有一个虚函数指针,这个虚函数表指针指向了虚函数表,虚函数表中存放的是虚函数的地址

在这里插入图片描述

在这里插入图片描述

什么时候才是多态?

在这里插入图片描述

在这里插入图片描述

这张图是继承中体现的多态性和虚函数的内容

在这里插入图片描述

面向对象的总结PPT

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值