C++面向对象最基本总结

  • Post author:
  • Post category:其他




补充:内存分区模型

栈:由编译器管理分配和回收,存放局部变量和函数参数。

堆:由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,空间较⼤,但可能会出现内存泄漏和空闲碎⽚的情况。

全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。

常量存储区:存储常量,⼀般不允许修改。

代码区:存放程序的⼆进制代码。


C++程序在执行时,将内存大方向划分为4个区域


代码区:存放函数体的二进制代码,由操作系统进行管理的

全局区:存放全局变量和静态变量以及常量

栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等

堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:

不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程


总结:


C++中在程序运行前分为全局区和代码区

代码区特点是共享和只读

全局区中存放全局变量、静态变量、常量

常量区中存放 const修饰的全局常量 和 字符串常量



函数重载

作用:函数名可以相同,提高复用性。

体现 编译时多态


函数重载满足条件:


同一个作用域下(同一个类中的同名函数,是 重载)


函数名称相同

函数参数类型不同 或者 个数不同 或者 顺序不同(不关心返回值类型)



1、类和对象



1 简述一下什么是面向对象

面向对象是一种编程思想,把一切东西看成是一个个对象,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示

面向过程和面向对象的区别

面向过程:根据业务逻辑从上到下写代码

面向对象:

将数据与函数绑定到一起,进行封装

,加快开发程序,减少重复代码的重写过程。


举例子


windows socket类 窗口类

点云类 PointCloud ,opencv

cv::Point2i, cv::Point2f, cv::Point2d, cv::Point3i, cv::Point3f, cv::Point3d.

面向对象的三大特征是封装、继承、多态。

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。封装本质上是一种管理,不想给别人看到的,我们使用protected/private把成员封装起来。

开放一些共有的成员函数对成员合理的访问。

继承:可以使用现有类的

所有数据成员和函数成员

功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。


C++认为

万事万物都皆为对象

,对象上有其属性和行为

//圆周率
# PI 3.14
const double PI = 3.14;
//1、封装的意义
//将属性和行为作为一个整体,用来表现生活中的事物
//封装一个圆类,求圆的周长
//class代表设计一个类,后面跟着的是类名
class Circle
{
//访问权限,默认 私有的
//属性
int m_r;//半径
//行为
//获取到圆的周长
public:
double calculateZC()
{
//2 * pi
* r
//获取圆的周长
return
2 * PI * m_r;
}
};
int main() {
//通过圆类,创建圆的对象
// c1就是一个具体的圆
Circle c1;
c1.m_r = 10; //给圆对象的半径 进行赋值操作
//2 * pi * 10 = = 62.8
cout << "圆的周长为: " << c1.calculateZC() << endl;system("pause");
return 0;
}



构造函数和析构函数 对象的初始化和清理

构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手

动调用。

析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供。



编译器提供的构造函数和析构函数是空实现。

空类A sizeof( A) 会实例化一个A类型的对象,为了确定唯一地址,返回1个字节


只含有 普通成员函数 或 析构函数,跟构造函数 还是返回1

类内的普通成员函数不参与sizeof()的统计。析构函数,跟构造函数这些成员函数,是跟sizeof无关的,也不难理解因为我们的sizeof是针对实例,而普通成员函数,是针对类体的,一个类的成员函数,多个实例也共用相同的函数指针,所以自然不能归为实例的大小。

在这里插入图片描述



空类不提供构造和析构,编译器会提 供 编译器提供的构造函数和析构函数是空实现

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

还会提供一个 赋值运算符


构造函数调用规则如下:

如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造

如果用户定义拷贝构造函数,c++不会再提供其他构造函数



C++中的空类默认产生哪些类成员函数



c++编译器至少给一个类添加6个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认复制构造函数,对属性进行值拷贝(浅拷贝)

4.赋值运算符重载函数,对属性进行值拷贝(浅拷贝)

5.取值运算符重载函数

6.const取值运算符重载函数


如果类中有属性指向堆区(如类有一个指针成员),做赋值操作时也会出现深浅拷贝问题

class Empty
{
public:
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};

只有当你需要用到这些函数的时候,编译器才会去定义它们?

在这里插入图片描述



深拷贝与浅拷贝

*

典型例子,类中含有一个指针成员 int

p;

这就是必要的时候 必须手动的写拷贝构造函数



浅拷贝:简单的赋值拷贝操作。只拷贝一个指针变量p 的拷贝

可能会导致同一个指针被释放2次

深拷贝:在堆区重新申请空间,拷贝指针p指向的对象。


https://blog.csdn.net/wue1206/article/details/81138097

class Test
{
private:
    int* p;
public:
    Test(int x)
    {
        this->p=new int(x);
        cout << "对象被创建" << endl;
    }
    ~Test()
    {
        if (p != NULL)
        {
            delete p;
        }
        cout << "对象被释放" << endl;
    }
    int getX() { return *p; }
    //深拷贝(拷贝构造函数)
    Test(const Test& a)
    {
        this->p = new int(*a.p);
        cout << "对象被创建" << endl;
    }
    //浅拷贝(拷贝构造函数)
    //Test(const Test& a)
    //{
    //  this->p = a.p;
    //  cout << "对象被创建" << endl;
    //}
};

int main()
{
    Test a(10);
    //我们手动的写拷贝构造函数,C++编译器会调用我们手动写的
    Test b = a;
    return 0;
}



初始化列表

Person p(1, 2, 3);
p.PrintPerson();



类对象作为类成员,考察构造析构顺序


https://blog.csdn.net/weixin_39731083/article/details/81903997


2. 构造函数的调用顺序

基类构造函数、对象成员构造函数、派生类本身的构造函数

  1. 析构函数的调用顺序

    派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好相反)


4. 特例


局部对象,在退出程序块时析构

静态对象,在定义所在文件结束时析构

全局对象,在程序结束时析构

继承对象,先析构派生类,再析构父类

对象成员,先析构类对象,再析构对象成员



4.2.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl;
Person p2;
p2.m_A = 200;
cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
cout << "p2.m_A = " << p2.m_A << endl;
//2、通过类名
cout << "m_A = " << Person::m_A << endl;
//cout << "m_B = " << Person::m_B << endl; //私有权限访问不到

静态成员分为:


静态成员变量


所有对象共享同一份数据

在编译阶段分配内存

类内声明,类外初始化


静态成员函数


所有对象共享同一个函数,可以通过类名 或者 对象名调用

静态成员函数只能访问静态成员变量,

并且静态成员函数 不能返回 对象本身



4.3 C++对象模型和this指针 在C++中,类内的成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储

(成员函数的代码应该反正代码区??)

只有非静态成员变量才属于类的对象的内存上,占用对象的内存空间



this指针概念

因为C++中成员变量和成员函数是分开存储的

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码

那么问题是:

这一块代码是如何区分那个对象调用自己的呢?

this指针

this指针 非静态成员函数通过

this指针

来区分是那个对象在调用自己。


this指针指向被调用的成员函数所属的对象


this指针是隐含每一个非静态成员函数内的一种指针

this指针不需要定义,直接使用即可


显式的this指针的用途:


1、当形参和成员变量同名时,可用this指针来区分

2、

在类的非静态成员函数中返回对象本身

,可使用return *this

class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
void test01()
{
	Person p1(10);
	cout << "p1.age = " << p1.age << endl;
	Person p2(10);
	p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
	cout << "p2.age = " << p2.age << endl;
}
int main() {
	test01();
	system("pause");
	return 0;
}



4.3.4 const修饰成员函数


const没有地方了,函数前面的位置被static/inline 占用,只能放在 函数后面,O(∩_∩)O哈哈~


常函数:


成员函数后加const后我们称为这个函数为常函数

常函数内不可以修改成员属性

成员属性声明时加关键字mutable后,在常函数中依然可以修改


常对象:


声明对象前加const称该对象为常对象

常对象只能调用常函数

class Person {
public:
	Person() {
		m_A = 0;
		m_B = 0;
	}

	//this指针的本质是一个指针常量,指针的指向不可修改
	//如果想让指针指向的值也不可以修改,需要声明常函数
	void ShowPerson() const {
		//const Type* const pointer;
		//this = NULL; //不能修改指针的指向 Person* const this;
		//this->mA = 100; //但是this指针指向的对象的数据是可以修改的

		//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
		this->m_B = 100;
	}

	void MyFunc() const {
		//mA = 10000;
	}

public:
	int m_A;
	mutable int m_B; //可修改 可变的
};


//const修饰对象  常对象
void test01() {

	const Person person; //常量对象  
	cout << person.m_A << endl;
	//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
	person.m_B = 100; //但是常对象可以修改mutable修饰成员变量

	//常对象访问成员函数
	person.MyFunc(); //常对象不能调用const的函数

}

int main() {

	test01();

	system("pause");

	return 0;
}



**4.4 友元 friend **


友元只是一个声明,完全不可能是这个类的成员


友元的目的 就是把一个函数或者类 声明为本类的友元,

他们就可以访问本类中的私有成员

友元的三种实现

全局函数做友元

类做友元

成员函数做友元

friend void goodGay(Building * building);
friend class goodGay;
friend void goodGay::visit();



4.5 运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型



4.6 继承

C++中的类,有三种访问权限(也称作访问控制),它们分别是public、protected、private

在这里插入图片描述

在这里插入图片描述


概念一:静态的


在C++中一个类的成员的访问级别可分为public,protected,和private。public的访问级别最低,谁都可以访问。

一个类的public成员变量、成员函数,可以通过类的成员函数、类的实例变量进行访问

protected访问级别中等,一个类的protected成员变量、成员函数,无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。

private访问级别最高, 一个类的private成员变量、成员函数,无法通过类的实例变量进行访问。但是可以通过类的友元函数、友元类进行访问。


到这里我们发现似乎protectd和private没有什么太大的区别,但是主要区别在概念二中。



概念二:动态的


关于动态的,也就是动作的意义。其中包含父子继承的关系。也就是自类是如何继承父类的,是public继承,protected继承,还是private继承。

(1)public继承

派生类通过public继承,基类的各种权限不变 。

派生类的成员函数,可以访问基类的public成员、protected成员,但是无法访问基类的private成员。

派生类的实例变量,可以访问基类的public成员,但是无法访问protected、private成员,仿佛基类的成员之间加到了派生类一般。

可以将public继承看成派生类将基类的public,protected成员囊括到派生类,但是不包括private成员。

(2)proteced继承

派生类通过protected继承,基类的public成员在派生类中的权限变成了protected 。protected和private不变。

派生类的成员函数,可以访问基类的public成员、protected成员,但是无法访问基类的private成员。

派生类的实例变量,无法访问基类的任何成员,因为基类的public成员在派生类中变成了protected。

可以将protected继承看成派生类将基类的public,protected成员囊括到派生类,全部作为派生类的protected成员,但是不包括private成员。

private成员是基类内部的隐私,除了友元,所有人员都不得窥探。派生类的友元,都不能访问

(3)private继承

派生类通过private继承,基类的所有成员在派生类中的权限变成了private。

派生类的成员函数,可以访问基类的public成员、protected成员,但是无法访问基类的private成员。

派生类的实例变量,无法访问基类的任何成员,因为基类的所有成员在派生类中变成了private。

可以将private继承看成派生类将基类的public,protected成员囊括到派生类,全部作为派生类的private成员,但是不包括private成员。

private成员是基类内部的隐私,除了友元,所有人员都不得窥探。派生类的友元,都不能访问



继承的好处:==可以减少重复的代码

class A : public B; 

A 类称为子类 或 派生类

B 类称为父类 或 基类


派生类中的成员,包含两大部分



一类是从基类继承过来的,一类是自己增加的成员。

从基类继承过过来的表现其共性,而新增的成员体现了其个性。



构造析构顺序


https://blog.csdn.net/weixin_39731083/article/details/81903997


2. 构造函数的调用顺序

基类构造函数、对象成员构造函数、派生类本身的构造函数

3. 析构函数的调用顺序

派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好相反)



4.6.5 继承同名成员处理方式



Overload(重载 ) Override(覆盖) (虚函数, 重写???) 3. Overwrite(改写)


https://www.cnblogs.com/kuliuheng/p/4107012.html



1、Overload(重载)


重载的概念最好理解,在同一个类声明范围中(在同一个作用域),定义了多个名称完全相同、参数(类型或者个数)不相同的函数,就称之为Overload(重载)。重载的特征如下:

(1)相同的

作用域

范围(在同一个类中);

(2)函数名字相同;

(3)参数不同;

(4)virtual 关键字可有可无。


2、Override((虚函数, 重写???)


Override的概念其实是用来实现C++多态性的,即 虚函数


在子类重新改写父类声明为virtual的函数

,在程序运行的时候,根据

对象的类型

来判断应该调用父类还是之类中的同名函数。


典型的例子

,在父类中,一般需要声明析构函数为virtual 函数。

避免内存泄漏


因为程序中,使用父类类型的指针指向子类对象,必须声明父类析构函数为virtual 函数。

Override(覆盖)的特征如下:

(1)不同的作用域(分别位于派生类与基类);

(2)函数名字相同;

(3)参数列表完全相同;

(4)基类函数必须有virtual 关键字。


3. Overwrite(改写)



父类中函数没有声明为虚函数,子类中改写了这个同名函数

改写是指

派生类的函数

屏蔽(或者称之为“隐藏”)了与其同名的基类函数。正是这个C++的隐藏规则使得问题的复杂性陡然增加,这里面分为两种情况讨论:

(1)如果派生类的函数与基类的函数同名,但是参数不同。那么此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)

如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。那么此时,基类的函数被隐藏(注意别与覆盖混淆)。

class Base{
public:
    virtual int fcn();
};
class Derived1:public Base{
public:
    //隐藏基类的fcn,这个fcn不是虚函数
    //Derived1继承了Base::fcn()的定义
    int fcn(int);         //形参类别与Base中的fcn不一样
    virtual void f2();    //是一个新的虚函数,在Base中不存在
};
class Derived2:public D1{
publicint fcn(int);        //是一个非虚函数,隐藏了Derived1::fcn(int)
    int fcn();           //覆盖了Base的虚函数fcn
    void f2();           //覆盖了Derived1的虚函数f2



4.6.6 继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加上 父类的 作用域
//同名成员属性
void test01()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	cout << "Son  下 m_A = " << s.m_A << endl;
	cout << "Base 下 m_A = " << s.Base::m_A << endl;

	//通过类名访问
	cout << "通过类名访问: " << endl;
	cout << "Son  下 m_A = " << Son::m_A << endl;
	cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}



4.6.7 多继承语法

C++允许

一个类继承多个类


语法:

class 子类 :继承方式 父类1 , 继承方式 父类2...


多继承可能会引发父类中有同名成员出现,需要加作用域区分


C++实际开发中不建议用多继承

//语法:class 子类:继承方式 父类1 ,继承方式 父类2 
class Son : public Base2, public Base1 
{
public:
	Son()
	{
		m_C = 300;
		m_D = 400;
	}
public:
	int m_C;
	int m_D;
};



4.6.8 菱形继承

注意:虚基类

//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo   : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义

  • 利用虚继承可以解决菱形继承问题,保证只有一份 虚基类中的 数据



4.7 多态

多态是C++面向对象三大特性之一**

多态分为两类



静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名 (overload)

动态多态: 派生类和虚函数实现运行时多态 (override关键字配合 virtual虚函数使用)

override是C++11中的一个新的继承控制关键字。override确保在派生类中声明的重载函数跟基类的 virtual虚函数有相同的声明。

override明确地表示一个函数是对基类中一个虚函数的重载。更重要的是,它会检查基类虚函数和派生类中重载函数的签名不匹配问题。如果签名不匹配,编译器会发出错误信息。

override表示函数应当重写基类中的虚函数(用于派生类的虚函数中)

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 – 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 – 运行阶段确定函数地址
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
总结
//多态满足条件: 
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象



4.7.3 纯虚函数和抽象类(或者叫纯虚类)

区别:抽象类(纯虚函数) 和 虚基类(虚继承)

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为

纯虚函数


纯虚函数语法:

virtual 返回值类型 函数名 (参数列表)= 0 ;


当类中有了纯虚函数,这个类也称为

抽象类


抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
	//纯虚函数
	//类中只要有一个纯虚函数就称为抽象类
	//抽象类无法实例化对象
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};



4.7.5 虚析构和纯虚析构



父类指针 指向 子类对象 带来的问题

多态使用时,如果子类中有属性开辟到堆区,那么

父类指针在释放时会调用父类的析构函数代码,无法调用到子类的析构代码


解决方式:将父类中的析构函数定义改为

虚析构

或者

纯虚析构


虚析构语法:


virtual ~类名(){}


纯虚析构语法:


virtual ~类名() = 0;


`类名::~类名(){}

​1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

3. 拥有纯虚析构函数的类也属于抽象类

class Animal {
public:
	Animal()
	{
		cout << "Animal 构造函数调用!" << endl;
	}
	virtual void Speak() = 0;
	//析构函数加上virtual关键字,变成虚析构函数
	//virtual ~Animal()
	//{
	//	cout << "Animal虚析构函数调用!" << endl;
	//}
	virtual ~Animal() = 0;
};

Animal::~Animal()
{
	cout << "Animal 纯虚析构函数调用!" << endl;
}

//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。

class Cat : public Animal {
public:
	Cat(string name)
	{
		cout << "Cat构造函数调用!" << endl;
		m_Name = new string(name);
	}
	virtual void Speak()
	{
		cout << *m_Name <<  "小猫在说话!" << endl;
	}
	~Cat()
	{
		cout << "Cat析构函数调用!" << endl;
		if (this->m_Name != NULL) {
			delete m_Name;
			m_Name = NULL;
		}
	}

public:
	string *m_Name;
};

void test01()
{
	Animal *animal = new Cat("Tom");
	animal->Speak();

	//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
	//怎么解决?给基类增加一个虚析构函数
	//虚析构函数就是用来解决通过父类指针释放子类对象
	delete animal;
}

int main() {

	test01();

	system("pause");

	return 0;
}



5 文件操作 流对象

是面向对象的思维;

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放通过

文件可以将数据持久化

C++中对文件操作需要包含头文件

< fstream >

文件类型分为两种:


  1. 文本文件

    – 文件以文本的

    ASCII码

    形式存储在计算机中

  2. 二进制文件

    – 文件以文本的

    二进制

    形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

3. ofstream:写操作

4. ifstream: 读操作

5. fstream : 读写操作



版权声明:本文为qq_46084757原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。