c++面向对象笔记

内存分区模型#

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

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:

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

1.1 程序运行前#

​ 在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

代码区:

​ 存放 CPU 执行的机器指令(二进制)

​ 特点:

​ 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可(只有一份)

​ 代码区是只读的,防止程序意外地修改了它的指令

全局区:

全局变量静态变量存放在此.

​ 全局区还包含了常量区, 字符串常量和其他常量也存放在此.

该区域的数据在程序结束后由操作系统释放.

示例:

//全局变量
int g_a = 10;
int g_b = 10;

//全局常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {

	//局部变量
	int a = 10;
	int b = 10;

	//打印地址
	cout << "局部变量a地址为: " << (int)&a << endl;
	cout << "局部变量b地址为: " << (int)&b << endl;

	cout << "全局变量g_a地址为: " <<  (int)&g_a << endl;
	cout << "全局变量g_b地址为: " <<  (int)&g_b << endl;

	//静态变量
	static int s_a = 10;
	static int s_b = 10;

	cout << "静态变量s_a地址为: " << (int)&s_a << endl;
	cout << "静态变量s_b地址为: " << (int)&s_b << endl;

	cout << "字符串常量地址为: " << (int)&"hello world" << endl;
	cout << "字符串常量地址为: " << (int)&"hello world1" << endl;

	cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl;
	cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;

	const int c_l_a = 10;
	const int c_l_b = 10;
	cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
	cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;

	system("pause");

	return 0;
}

总结:

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放 const修饰的全局常量 和 字符串常量

1.2 程序运行后#

栈区:

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

​ 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

示例:

int * func()
{
	int a = 10;
	return &a;
}

int main() {

	int *p = func();

	cout << *p << endl;
	cout << *p << endl;

	system("pause");

	return 0;
}

堆区:

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

​ 在C++中主要利用new在堆区开辟内存

示例:

int* func()
{
	int* a = new int(10);
	return a;
}

int main() {

	int *p = func();

	cout << *p << endl;
	cout << *p << endl;
    
	system("pause");

	return 0;
}

总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内存

10 引用#

10.1 引用的基本使用#

作用: 给变量起别名

语法: 数据类型 &别名 = 原名

用别名操作和用原名一样。

示例:

int main() {
	int a = 10;
	int &b = a;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	b = 100;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	return 0;
}

注意

  • 引用必须初始化
  • 引用在初始化后,不可以改变

10.2 引用做函数参数#

作用: 可以改实参;节省内存
优点: 可以简化指针修改实参

示例:

//1. 值传递
void mySwap01(int a, int b) {
	int temp = a;
	a = b;
	b = temp;
}

//2. 地址传递
void mySwap02(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//3. 引用传递
void mySwap03(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main() {

	int a = 10;
	int b = 20;

	mySwap01(a, b);
	cout << "a:" << a << " b:" << b << endl;

	mySwap02(&a, &b);
	cout << "a:" << a << " b:" << b << endl;

	mySwap03(a, b);
	cout << "a:" << a << " b:" << b << endl;
	return 0;
}

总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单

10.3 引用做函数返回值#

注意:不要返回局部变量引用
(也不能返回局部变量指针)
示例:

//返回静态变量引用
int& test02() {
	static int a = 20;
	return a;
}
int main() {
	//如果函数做左值,那么必须返回引用
	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	test02() = 1000;

	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;
	return 0;
}

ref2 = 20
ref2 = 20
ref2 = 1000
ref2 = 1000

int & maxr(int &m, int &n) {
	if(m>n)
		return m; //返回变量(可作左值)
	return n; 
}
int main(){
	int a=3, b=5, c=0;
	cout<<"a,b,c = "<<a<<", "<<b<<", "<<c<<endl;
	//输出: a, b, c = 3, 5, 0
	c= maxr(a,b);//返回引用,即变量b(以及b的值) 
	cout<<"a,b,c = "<<a<<", "<<b<<", "<<c<<endl; //输出: a, b, c = 3, 5, 5
	maxr(a,b)=2;//返回引用,即变量b,而后为b重新赋值 
	cout<<"a,b,c="<<a<<", "<<b<<", "<<c<<endl; //输出: a, b, c = 3, 2, 5
	maxr(a,b)++;//返回引用,即变量b,而后实现b值加1 
	cout<<"a,b,c = "<<a<<", "<<b<<", "<<c<<endl; //输出: a, b, c = 4, 2, 5
}

10.4 引用的本质#

本质:引用的本质在c++内部实现是一个指针常量.

讲解示例:

//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
	ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
	int a = 10;
    
    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
	int& ref = a; 
	ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
    
	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;
    
	func(a);
	return 0;
}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

10.5 右值引用#

int &x=10;//错,必须引用变量
int &&x=10;//对,这是右值的引用

右值引用(rvalue reference)确实是用来直接移动对象的,而不是复制对象。当我们使用右值引用时,我们是在请求原始对象的所有权转移,这意味着原始对象的数据成员将被移动到新的位置,而不是被复制。

#include <iostream>

class MyClass {
public:
    MyClass(int value) : value(value) {}

    // 移动构造函数
    MyClass(MyClass &&other) noexcept : value(other.value) {
        other.value = 0; // 保证 other 在移动后不再有效
    }

    int getValue() const { return value; }

private:
    int value;
};

MyClass createMyClass(int value) {
    return MyClass(value); // 返回临时对象
}

int main() {
    MyClass obj1 = createMyClass(10); // obj1 是通过右值引用接收临时对象
    MyClass obj2 = std::move(obj1); // obj2 通过右值引用接收 obj1 的值

    std::cout << "obj1 value: " << obj1.getValue() << std::endl; // 输出 obj1 的值
    std::cout << "obj2 value: " << obj2.getValue() << std::endl; // 输出 obj2 的值

    // obj1 的值现在是未定义的,因为它的数据已经被移动到 obj2
}

10.6 常量引用#

作用: 常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰形参,防止形参改变实参
示例:

//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
	//v += 10;
	cout << v << endl;
}

int main() {

	//int& ref = 10;  引用本身需要一个合法的内存空间,因此这行错误
	//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
	const int& ref = 10;

	//ref = 100;  //加入const后不可以修改变量
	cout << ref << endl;

	//函数中利用常量引用防止误操作修改实参
	int a = 10;
	showValue(a);

	system("pause");

	return 0;
}

11 类和对象#

C++面向对象的三大特性为:封装、继承、多态

类 看成自定义的数据类型,对象 看成变量

• 类定义文件 .h
• 类的成员函数定义文件 .cpp
• 主函数文件 .cpp

1.1 类#

语法: class 类名{ 访问权限: 属性 / 行为 };

一般类名首字母大写

类的成员:
成员变量(数据成员)
成员函数
凡在类中定义的函数成员均隐含为内联函数
类定义外函数名的前面必须用“<类名>::”来限定
数据类型 类名::函数名()

定义类时系统不为类分配存储空间,所以不能对类的数据成员初始化.
类中的任何数据成员也不能使用关键字extern限定其存储类型。

class Circle {
public:
    int m_r;//半径
    double calculateZC() {
        return 2 * PI * m_r;
    }
};
class Circle {
public://访问权限  公共的权限
    int m_r;//半径
    double calculateZC();
};
double Circle::calculateZC() {
	return 2 * PI * m_r;
}

1.2 对象#

类的对象(object)(或称该类的实例)的说明语句格式为:
<类名><对象名1>,…,<对象名n>;

按如下方式来使用对象的成员(数据成员或函数成员):
<对象名>.<成员名>#### 访问权限

类的访问权限有三种:

  1. public 公共权限 类内可以访问 类外可以访问
  2. protected 保护权限 类内和派生类可以访问,类外不可以访问
  3. private 私有权限 类内可以访问 类外不可以访问

默认为 private,左括号紧跟着的第一个为private可省略不写

示例:

class Person
{
	//姓名  公共权限
public:
	string m_Name;

	//汽车  保护权限
protected:
	string m_Car;

	//银行卡密码  私有权限
private:
	int m_Password;

public:
	void func()
	{
		m_Name = "张三";
		m_Car = "拖拉机";
		m_Password = 123456;
	}
};

int main() {

	Person p;
	p.m_Name = "李四";
	//p.m_Car = "奔驰";  //保护权限类外访问不到
	//p.m_Password = 123; //私有权限类外访问不到

	return 0;
}

struct和class区别

在C++中 struct和class唯一的区别就在于 默认的访问权限不同

区别:

  • struct 默认权限为公共
  • class 默认权限为私有
class C1 {
	int  m_A; //默认是私有权限
};

struct C2{
	int m_A;  //默认是公共权限
};

int main() {

	C1 c1;
	c1.m_A = 10; //错误,访问权限是私有

	C2 c2;
	c2.m_A = 10; //正确,访问权限是公共

	return 0;
}

成员属性设置为私有的优点:可以自己控制读写权限

改成多文件#

1.创头文件 point.h

#pragma once// 防止头文件重复
#include <iostream>
using namespace std;
class Point
{
private:
	double m_x,m_y;	
public:
	void set(double x,double y);//函数删除改分号
	double get_x();
	double get_y();

};
 

circle.h同理

#pragma once
#include <iostream>
#include "point.h"
using namespace std;

class Circle
{
private:
	double m_x,m_y,m_r;
public:
	void set(double x,double y,double r);
	void weizhi(Point a);
};

2.创源文件 point.cpp

只留下函数,加上作用域

#include "point.h"

	void Point::set(double x,double y)
	{
		m_x=x;m_y=y;
	}
	double Point::get_x()	{ return m_x; }
	double Point::get_y()	{ return m_y; }

circle.cpp

#include "circle.h"
#include <cmath>

	void Circle::set(double x,double y,double r)
	{
		m_r=r;m_x=x;m_y=y;
	}
	void Circle::weizhi(Point a)
	{
		double dis=sqrt( (a.get_x()-m_x)*(a.get_x()-m_x) + (a.get_y()-m_y)*(a.get_y()-m_y));
		if(dis<m_r) cout<<"1 点在圆内";
		else if(dis==m_r) cout<<"2 点在圆上";
		else cout<<"3 点在圆外";
	}

3 main.cpp

#include <iostream>
#include <string>
#include <cmath>
#include "point.h"
#include "circle.h"
using namespace std;


int main() {
	Point p;
	p.set(1,2);

	Circle c;
	c.set(2,2,1);

	c.weizhi(p);
	
	return 0;
}

1.3 this指针#

this 指针是一个特殊的指针,它指向当前对象的实例

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

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

友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。

this指针的用途:

  • 当形参和类成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身
class Person {  
public:  
  
    Person(int age) {  
        //1、当形参和成员变量同名时,可用this指针来区分  
        this->age = age;  
        //this->age相当于p1.age
    }  
  
    Person &PersonAddPerson(Person p) {  
        this->age += p.age;  
        //返回对象本身  
        return *this;  
    }  
  
    int age;  
};  
  
int main() {  
    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;  
    return 0;  
}

2.构造函数和析构函数#

2.1 概述#

注意:权限为public

对象的初始化和清理工作是编译器强制要我们做的事情,如果我们不自己写构造和析构,编译器会提供空实现

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

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

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

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

  • 构造函数:主要作用在于创建对象时为对象初始化,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

2.2构造函数#

用于对象的初始化

对类对象进行初始化时,判断有无自定义构造函数

  • 包含自定义构造函数时,默认构造函数失效
  • 无自定义构造函数,用默认构造函数初始化对象

构造函数语法:类名(){}

  1. 构造函数,没有返回值也没有返回类型
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在新对象建立时候会自动调用构造,无须手动调用,而且只会调用一次

“ 新对象建立 〞 包括两种情况 :
 1. 通过对象说明语句创建
 2. 用new创建新的动态对象

class Person{
    string name;
public:
    //构造函数
    Person(){
        puts("调用构造函数");
    }
};

int main() {
    Person p1; //1
    // Person *p2;这样是不会调用构造的
    Person *p2=new Person;//2
    return 0;
}

初始化列表

<类名>(int 变量名1,int 变量名2):成员变量1(变量名1),成员变量2(变量名2){}

等同于以下语法

<类名>(int 变量名1,int 变量名2){
	成员变量1=变量名1;
	成员变量2=变量名2;
}

带有一个参数的构造函数,将参数类型隐式转换为相应的类类型。

可使用explicit关键字去掉隐式转换机制
(会报错)

构造函数的分类及调用#

两种分类方式:
​ 按参数分为: 有参构造和无参构造
​ 按类型分为: 普通构造和拷贝构造

三种调用方式:
​ 括号法
​ 显示法
​ 隐式转换法

示例:

//1、构造函数分类
// 按照参数分类分为 有参和无参构造   无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int a) {
		age = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	Person(const Person& p) {
		age = p.age;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
	Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

	//2.1  括号法,常用
	Person p1(10);
	//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
	//Person p2();

	//2.2 显式法
	Person p2 = Person(10); 
	Person p3 = Person(p2);
	//Person(10)单独写就是匿名对象  当前行结束之后,马上析构

	//2.3 隐式转换法
	Person p4 = 10; // Person p4 = Person(10); 
	Person p5 = p4; // Person p5 = Person(p4); 

	//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
	//Person p5(p4);
}

int main() {

	test01();
	//test02();
	return 0;
}
拷贝构造函数#

用己存在的同一类 的对象来对它进行初始化。
拷贝构造函数只含有一个形参,而且其形参为本类对象的引用。

系统会自动生成缺省的拷贝构造函数,实现对象间的拷贝(浅拷贝)

拷贝构造函数调用时机通常有三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象(已经都初始化过的再赋值就不执行拷贝构造了)
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

示例:

class Person {
public:
	Person() {
		cout << "无参构造函数!" << endl;
		mAge = 0;
	}
	Person(int age) {
		cout << "有参构造函数!" << endl;
		mAge = age;
	}
	Person(const Person& p) {
		cout << "拷贝构造函数!" << endl;
		mAge = p.mAge;
	}
	//析构函数在释放内存之前调用
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
	Person man(100); //p对象已经创建完毕
	Person newman(man); //拷贝构造1
	//也可以像下面这么写
	//Person newman2 = man; 
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
	Person p; //无参构造函数
	doWork(p);
}

//3. 以值方式返回局部对象
Person doWork2() {
	Person p1;
	cout << (int *)&p1 << endl;
	return p1;
}

void test03() {
	Person p = doWork2();
	cout << (int *)&p << endl;
}

int main() {
	//test01();
	//test02();
	test03();
	return 0;
}
浅拷贝&深拷贝#

系统会自动生成缺省的拷贝构造函数,实现对象间的拷贝(浅拷贝)
但有问题,比如下面的例子:

#include <fstream>
#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    Person() {
        cout << "无参构造函数!" << endl;
    }
    Person(int age, int height) {
        cout << "有参构造函数!" << endl;
        m_age = age;
        m_height = new int(height);//堆区开辟的数据,需要手动释放
    }
    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
            delete m_height;
    }

public:
    int m_age;
    int *m_height;
};

void test01() {
    Person p1(18, 180);
    Person p2(p1);//栈满足先进后出,p1先构造,晚析构,所以析构顺序是先p2再p1
    cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
    cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {
    test01();
    return 0;
}

报错
Double free of object 0x11fe06a90
原因是先执行p2析构之后,m_height被释放了,再执行p1析构的又被释放了,但同一个内存,不能被释放两次

浅拷贝没有再开一个内存拷贝过去,
解决办法就是手写一个拷贝构造函数进行深拷贝

浅拷贝 拷贝的是地址,修改拷贝后的东西,拷贝前的东西也会变
深拷贝 拷贝的是数据,新开一个全新的地址

    Person(const Person &p) {
        cout << "拷贝构造函数!" << endl;
        //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        m_age = p.m_age;
        m_height = new int(*p.m_height);
    }
    
委托构造函数#

一个构造函数在其初始化列表中调用了另一个构造函数,即将构造工作委托给另一个构造函数

class Cube{
    int a,b,c;
public:
    //构造函数
    Cube(int a_,int b_,int c_):a(a_),b(b_),c(c_){}
    //委托构造
    Cube(int x):Cube(x,x,x){}
    void show(){
        cout<<a<<" "<<b<<" "<<c<<endl;
    }
};

int main() {
    Cube c1(2);
    c1.show();
    return 0;
}

2.3 析构函数#

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也没有返回类型
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 当对象退出其说明区域, 或使用delete释放动态对象 时,系统自动调用其析构函数
  5. 一个类只能有一个析构函数

先构造的后析构,后构造的先析构

class Stu {
public:
    ~Stu() {
        cout << "析构函数" << name << endl;
    }
    string name;
    //使用初始化列表来初始化字段
    Stu(string a) : name(a) { cout << "构造函数" << name << endl; }
};
int main() {
    string a, b, c;
    cin >> a >> b >> c;
    Stu s1(a), s2(b);
    Stu *s3 = new Stu(c);
    delete s3;
    return 0;
}  /*
input:
a b c
output:
构造函数a
构造函数b
构造函数c
析构函数c
析构函数b
析构函数a

new的需要手动delete,不会自动析构
*/

2.4 常对象与常量成员#

常对象#

类类型的const变量
const <类名> <常对象名>[(实参表)]
构成常对象的任何成员变量(不包括由mutable修饰的)都不能被修改,但是可以读取成员变量值。

常对象不能够调用任何普通成员函数,可以调用常量成员函数

常量数据成员#

必须进行初始化,而且只能通过构造函数的成员初始化列表的方式来进行

常量函数成员#

<类型说明符> <函数名> ( <参数表> ) const {<函数体>}

只有权读取相应对象(即对象this)的内容,但无权修改它们。

2.5 类对象作为类成员#

C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员

例如:

class A {}
class B
{
    A a;
}

B类中有对象A作为成员,A为对象成员

那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?
顺序为:A构造,B构造,B析构,A析构

示例:

#include <iostream>  
using namespace std;  
  
class Phone {  
public:  
    Phone(string name) {  
        m_PhoneName = name;  
        cout << "Phone构造" << endl;  
    }  
    ~Phone() {  
        cout << "Phone析构" << endl;  
    }  
    string m_PhoneName;  
  
};  
  
class Person {  
public:  
    Person(string name, string pName) : m_Name(name), m_Phone(pName) {  
        cout << "Person构造" << endl;  
    }  
    ~Person() {  
        cout << "Person析构" << endl;  
    }  
  
    void playGame() {  
        cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;  
    }  
    string m_Name;  
    Phone m_Phone;  
};  
  
void test01() {  
    Person p("张三", "苹果");  
    p.playGame();  
}  
  
int main() {  
    test01();  
    return 0;  
}

对于new的对象,delete时析构;
无名对象用完直接析构

Person ("矮矮");//匿名

静态对象,程序结束才释放

2.6 静态成员#

static 修饰的类成员说明称为类的静态成员。

类的静态成员为其所有对象共享, 不管有多少对象,静态成员只有一份存于公用内存中。

静态成员分为:

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,(必须)类外初始化<类型> <类名>::<静态数据成员> = <初值>;
    • 可以通过类名直接访问
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
    • 没有this 指针

示例1 : 静态成员变量

静态数据成员的定义和初始化通常放在类的实现文件(.cpp)中,而不是头文件(.h或.hpp)中,以避免重复定义。

示例2: 静态成员函数

  
class Person {  
public:  
    static void func() {  
        cout << "func调用" << endl;  
        m_A = 100;  
        //m_B = 100; //错误,不可以访问非静态成员变量  
    }  
    static int m_A; //静态成员变量  
    int m_B;  
  
private:  
    //静态成员函数也是有访问权限的  
    static void func2() {  
        cout << "func2调用" << endl;  
    }  
};  
  
int Person::m_A = 10;  
  
int main() {  
    //静态成员函数两种访问方式  
  
    //1、通过对象  
    Person p1;  
    p1.func();  
  
    //2、通过类名  
    Person::func();  
  
    //Person::func2(); //私有权限访问不到  
    return 0;  
}

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

只有非静态成员变量才占对象空间

class Person {
public:
	Person() {
		mA = 0;
	}
	//非静态成员变量占对象空间
	int mA;
	//静态成员变量不占对象空间
	static int mB; 
	//函数也不占对象空间,所有函数共享一个函数实例
	void func() {
		cout << "mA:" << this->mA << endl;
	}
	//静态成员函数也不占对象空间
	static void sfunc() {
	}
};

int main() {
	cout << sizeof(Person) << endl;
	return 0;
}

2.7 空指针访问成员函数#

空指针也是可以调用成员函数的

但如果成员函数中用到this指针,就不可以了

示例:

//空指针访问成员函数
class Person {
public:

	void ShowClassName() {
		cout << "我是Person类!" << endl;
	}

	void ShowPerson() {
		if (this == NULL) {
			return;
		}
		cout << mAge << endl;
	}

public:
	int mAge;
};

int main() {
	Person * p = NULL;
	p->ShowClassName(); //空指针,可以调用成员函数
	p->ShowPerson();  //但是如果成员函数中用到了this指针,就不可以了
	return 0;
}

常函数:

  • 成员函数后加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;
}

3 友元#

在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元。

友元函数可以访问类的所有成员

友元的关键字为 friend
说明格式:
friend <返回值类型> <函数名>( <参数表> );

友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。

3.1 全局函数做友元#

class Building {  
    //goodGay全局函数,可以访问building类中的私有内容  
    friend void goodGay(Building *building);  
  
public:  
  
    Building() {  
        this->m_SittingRoom = "客厅";  
        this->m_BedRoom = "卧室";  
    }  
  
public:  
    string m_SittingRoom; //客厅  
  
private:  
    string m_BedRoom; //卧室  
};  
  
void goodGay(Building *building) {  
    cout << "好基友正在访问: " << building->m_SittingRoom << endl;  
    cout << "好基友正在访问: " << building->m_BedRoom << endl;  
}  
  
  
int main() {  
    Building b;  
    goodGay(&b);  
    return 0;

3.2 类做友元#

将一个类B说明为另一个类A的友元类,类B中的 所有函数都是类A的友元函数,可以访问类A中的 所有成员

友元类的关系是单向的,如果说明类B是类A的友元类,不等于类A也是类B的友元类

友元类的关系不能传递,如果类B是类A的友元类 ,而类C是类B的友元类,不等于类C是类A的友元类

除非确有必要,一般不把整个类说明为友元类,而把成员函数说明为友元函数

class Building;  
  
class goodGay {  
public:  
    goodGay();  
  
    void visit();  
  
private:  
    Building *building;  
};  
  
  
class Building {  
    //告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容  
    friend class goodGay;  
  
public:  
    Building() {  
        this->m_SittingRoom = "客厅";  
        this->m_BedRoom = "卧室";  
    }  
  
public:  
    string m_SittingRoom; //客厅  
private:  
    string m_BedRoom;//卧室  
};  
  
goodGay::goodGay() {  
    building = new Building;  
}  
  
void goodGay::visit() {  
    cout << "好基友正在访问" << building->m_SittingRoom << endl;  
    cout << "好基友正在访问" << building->m_BedRoom << endl;  
}  
  
int main() {  
    goodGay gg;  
    gg.visit();  
    return 0;  
}

3.3 成员函数做友元#

  
class Building;  
  
class goodGay {  
public:  
  
    goodGay();  
  
    void visit(); //只让visit函数作为Building的好朋友,可以发访问Building中私有内容  
    void visit2();  
  
private:  
    Building *building;  
};  
  
class Building {  
    //告诉编译器  goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容  
    friend void goodGay::visit();  
  
public:  
    Building();  
  
public:  
    string m_SittingRoom; //客厅  
private:  
    string m_BedRoom;//卧室  
};  
  
Building::Building() {  
    this->m_SittingRoom = "客厅";  
    this->m_BedRoom = "卧室";  
}  
  
goodGay::goodGay() {  
    building = new Building;  
}  
  
void goodGay::visit() {  
    cout << "好基友正在访问" << building->m_SittingRoom << endl;  
    cout << "好基友正在访问" << building->m_BedRoom << endl;  
}  
  
void goodGay::visit2() {  
    cout << "好基友正在访问" << building->m_SittingRoom << endl;  
    //cout << "好基友正在访问" << building->m_BedRoom << endl;  
}  
  
int main() {  
    goodGay gg;  
    gg.visit();  
    return 0;  
}

4 类中运算符重载#

允许使用如下两种不同方式来定义运算符重载函数:

  1. 以类的友元函数方式定义
    所有运算分量必须显式地列在本友元函数的参数表中,而且这 些参数类型中至少要有一个应该是说明该友元的类类型或是对该类的引用
  2. 以类的公有成员函数方式定义
    总以当前调用者对象(this)作为该成员函数的隐式第一运算分量,若所定义的运算多于一个运算对象时,才将其余运算对象显式地列在该成员函数的参数表中

如下运算符不可重载: : :: ?: . sizeof

只能以类成员而不能以友元身份重载的运算符:
= () [] typedef ->

不改变原运算符的优先级和结合性,也不改变运算符的语法结构(单目运算符只能重载为单目运算符)

总结: 前置递增返回引用,后置递增返回值

赋值运算符重载#

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

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=, 对属性进行值拷贝
    如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题

示例:

class Person {  
public:  
    Person(int age) {  
        //将年龄数据开辟到堆区  
        m_Age = new int(age);  
    }  
  
    //重载赋值运算符   
	Person &operator=(Person &p) {  
        if (m_Age != nullptr) {  
            delete m_Age;  
            m_Age = nullptr;  
        }  
        //编译器提供的代码是浅拷贝  
        //m_Age = p.m_Age;  
  
        //提供深拷贝 解决浅拷贝的问题  
        m_Age = new int(*p.m_Age);  
  
        //返回自身  
        return *this;  
    }  
  
  
    ~Person() {  
        if (m_Age != nullptr) {  
            delete m_Age;  
            m_Age = nullptr;  
        }  
    }  
    //年龄的指针  
    int *m_Age;  
};  
  
void test01() {  
    Person p1(18);  
  
    Person p2(20);  
  
    Person p3(30);  
  
    p3 = p2 = p1; //赋值操作  
  
    cout << "p1的年龄为:" << *p1.m_Age << endl;  
  
    cout << "p2的年龄为:" << *p2.m_Age << endl;  
  
    cout << "p3的年龄为:" << *p3.m_Age << endl;  
}  
  
int main() {  
    test01();  
  
    //int a = 10;  
    //int b = 20;    //int c = 30;  
    //c = b = a;    //cout << "a = " << a << endl;    //cout << "b = " << b << endl;    //cout << "c = " << c << endl;    return 0;  
}

关系运算符重载#

作用: 重载关系运算符,可以让两个自定义类型对象进行对比操作
示例:

class Person {  
public:  
    Person(string name, int age) {  
        this->m_Name = name;  
        this->m_Age = age;  
    };  
    bool operator==(Person &p) {  
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {  
            return true;  
        } else {  
            return false;  
        }  
    }  
    bool operator!=(Person &p) {  
        if (this->m_Name == p.m_Name && this->m_Age == p.m_Age) {  
            return false;  
        } else {  
            return true;  
        }  
    }  
  
    string m_Name;  
    int m_Age;  
};  
  
int main() {  
    //int a = 0;  
    //int b = 0;  
    Person a("孙悟空", 18);  
    Person b("孙悟空", 18);  
  
    if (a == b) {  
        cout << "a和b相等" << endl;  
    } else {  
        cout << "a和b不相等" << endl;  
    }  
  
    if (a != b) {  
        cout << "a和b不相等" << endl;  
    } else {  
        cout << "a和b相等" << endl;  
    }  
    return 0;  
}

函数调用运算符重载#

  • 函数调用运算符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活

示例:

class MyPrint {  
public:  
    void operator()(string text) {  
        cout << text << endl;  
    }  
};  
  
void test01() {  
    //重载的()操作符 也称为仿函数  
    MyPrint myFunc;  
    myFunc("hello world");  
}  
  
class MyAdd {  
public:  
    int operator()(int v1, int v2) {  
        return v1 + v2;  
    }  
};  
  
void test02() {  
    MyAdd add;  
    int ret = add(10, 10);  
    cout << "ret = " << ret << endl;  
    //匿名对象调用    
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;  
}  
  
int main() {  
  
    test01();  
    test02();  
  
    return 0;  
}

oj题目代码

#include <cstdio>
#include <iostream>

using namespace std;
int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}
int lcm(int a, int b) {
    return a * b / gcd(a, b);
}
class Rational {
private:
    int fz, fm;

public:
    Rational() {
        fz = 0, fm = 1;
    }
    Rational(int z, int m) {
        if (!z) {
            fz = 0, m = 1;
        } else {
            int tmp = gcd(z, m);
            fz = z / tmp;
            fm = m / tmp;
        }
    }

    Rational operator+(Rational &x) {
        //分母通分
        int tmp = lcm(this->fm, x.fm);
        Rational res = Rational(tmp / this->fm * this->fz + tmp / x.fm * x.fz, tmp);
        return res;
    }
    Rational operator-(Rational &x) {
        //分母通分
        int tmp = lcm(this->fm, x.fm);
        Rational res = Rational(tmp / this->fm * this->fz - tmp / x.fm * x.fz, tmp);
        return res;
    }
    friend Rational operator*(Rational &x, Rational &y);
    friend Rational operator/(Rational &x, Rational &y);
    friend ostream &operator<<(ostream &out, Rational &x);

    Rational operator++() {
        this->fz += this->fm;
        if (!this->fz) {
            this->fm = 1;
        } else {
            int tmp = gcd(this->fz, this->fm);
            this->fz /= tmp;
            this->fm /= tmp;
        }
        return *this;//返回引用
    }
    Rational operator++(int) {//后置
        Rational res = *this;
        this->fz += this->fm;
        return res;// 返回值
    }
    Rational operator=(const Rational &x) {
        Rational y = Rational(x.fz, x.fm);
        this->fz = y.fz;
        this->fm = y.fm;
        return *this;
    }
};
//友元没有this指针
Rational operator*(Rational &x, Rational &y) {
    Rational res = Rational(x.fz * y.fz, x.fm * y.fm);
    return res;
}
Rational operator/(Rational &x, Rational &y) {
    Rational res = Rational(x.fz * y.fm, x.fm * y.fz);
    return res;
}
//ostream对象只能有一个,所以用引用,ostream是cout的数据类型
//只能用全局函数重载
ostream &operator<<(ostream &out, Rational &x) {
    if (x.fm == 1) {
        out << x.fz;
    } else {
        if (x.fm < 0) {
            x.fm *= -1;
       x.fz *= -1;
        }
        out << x.fz << "/" << x.fm;
    }
    return out;
}
int main() {

    Rational R1, R2;
    int fenzi, fenmu;
    cin >> fenzi >> fenmu;
    Rational x = Rational(fenzi, fenmu);
    cin >> fenzi >> fenmu;
    Rational y = Rational(fenzi, fenmu);
    Rational res = x + y;
    cout << res << endl;
    res = x - y;
    cout << res << endl;
    res = x * y;
    cout << res << endl;
    res = x / y;
    cout << res << endl;
    res = x++;
    cout << res << " ";
    res = y++;
    cout << res << endl;
    res = Rational(1, 1);
    R1 = res / x;
    R2 = res / y;
    cout << R1 << " " << R2;
    return 0;
}

5 继承#

继承是面向对象三大特性之一

有些类与类之间存在特殊的关系,例如下图中:

1544861202252

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。

这个时候我们就可以考虑利用继承的技术,减少重复代码

5.1#

继承的基本语法

class<派生类名>: 派生方式 基类名1,派生方式 基类名2……{};

class animal{  
public:  
    void eat(){  
        cout<<"能吃"<<endl;  
    }  
    void sleep(){  
        cout<<"能睡"<<endl;  
    }  
};  
  
class dog:public animal{  
public:  
    void bark(){  
        cout<<"旺旺叫"<<endl;  
    }  
};  
int main() {  
    dog p1;  
    p1.eat();  
    p1.sleep();  
    p1.bark();  
    return 0;  
}

对派生类而言,不加类名限定时 默认 为是处理派生类成员,而要访问基类重名成员时,则要通过类名限定

在类定义时,使用关键字final限定,则该类不允许任何类继承

class Box final{

};

不论是数据成员,还是函数成员(非私有成员),除构造函数与析构函数外全盘接收

声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为同名覆盖(override)

派生类可以赋值给基类,反之不行。

5.2 继承方式#

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

img

派生类的访问权限

派生方式 基类中的存取权限 派生类中的被继承方式
public public public
public protected protected
public private (inaccessible)
protected public protected
protected protected protected
protected private (inaccessible)
private public private
private protected private
private private (inaccessible)

我们可以根据访问权限总结出不同的访问类型,如下所示:

访问 public protected private
同一个类 yes yes yes
派生类 yes yes no
外部的类 yes no no

5.3 继承中构造和析构顺序#

派生类继承基类后,当创建派生类对象,也会调用基类的构造函数

问题:基类和派生类的构造和析构顺序是谁先谁后?
先调用基类构造函数,再调用派生类构造函数,析构顺序与构造相反

示例:

class Base {  
public:  
    Base() {  
        cout << "Base构造函数!" << endl;  
    }  
  
    ~Base() {  
        cout << "Base析构函数!" << endl;  
    }  
};  
  
class Son : public Base {  
public:  
    Son() {  
        cout << "Son构造函数!" << endl;  
    }  
  
    ~Son() {  
        cout << "Son析构函数!" << endl;  
    }  
};  
  
int main() {  
    Son s;  
    return 0;  
}

输出:

Base构造函数!
Son构造函数!
Son析构函数!
Base析构函数!

派生类继承自两个基类

#include <iostream>
using namespace std;
class B {
public:
    B() { cout << "B() is called!" << endl; }
};
class D1 : public B {
public:
    D1() { cout << "D1() is called!" << endl; }
};
class D2 : public B {
public:
    D2() { cout << "D2() is called!" << endl; }
};
class DD : public D2, public D1 {
public:
    DD() : D1(), D2() { cout << "DD() is called!" << endl; }
};
int main() {
    DD dd;
    return 0;
}

构造顺序

B() is called!
D2() is called!
B() is called!
D1() is called!
DD() is called!

5.4 继承同名成员处理方式#

问题:当派生类与基类出现同名的成员,如何通过派生类对象,访问到派生类或基类中同名的数据呢?

  • 访问派生类同名成员 直接访问即可
  • 访问基类同名成员 需要加作用域
s.Base::m_A //成员变量
s.Base::func(10); //成员函数

5.5 派生类的拷贝构造函数#

派生类的拷贝构造函数,可以在成员初始化符表 位置调用基类的拷贝构造函数,“拷贝”派生类 中的基类部分;如果不显式调用基类的拷贝构造函数(显示调用是深拷贝),将自动调用基类的无参构造函数(如果 有定义)或默认构造函数(如果有效)为派生类 创建基类部分

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
using namespace std;
class Box
{
protected:
    double length;
    double width;
    double height;

public:
    // 自定义构造
    Box(double lv, double wv, double hv) : length{lv}, width{wv}, height{hv}
    {
        cout << "Box(double, double, double) called.\n";
    }
    // 委托构造
    Box(double side) : Box(side, side, side)
    {
        cout << "Box(double) called.\n";
    }
    // 拷贝构造
    Box(const Box &box) : length{box.length}, width{box.width}, height{box.height}
    {
        cout << "Box copy constructor" << endl;
    }
    // 无参构造
    Box()
    {
        length = width = height = 1.0;
        cout << "Box() called.\n";
    }
    double volume() const
    {
        return length * width * height;
    }
};
class Carton : public Box
{
private:
    string material;

public:
    // Carton(double lv, double wv, double hv, string mat)
    //     : length{lv}, width{wv}, height{hv}, material{mat}
    // {
    //     cout << "Carton(double,double,double,string) called.\n";
    // }1. ERROR,基类成员变量(length不能放到成员初始化符表中,但可以在函数体中访问
    Carton(double lv, double wv, double hv, string mat) : Box{lv, wv, hv}, material{mat}
    {
        cout << "Carton(double,double,double,string) called.\n";
    }
    // Carton(const Carton &carton) : material{carton.material}
    // {
    //     cout << "Carton copy constructor" << endl;
    // }2.这里没有调用基类的拷贝构造,会调用基类的无参构造 
    Carton(const Carton &carton) : Box(carton), material{carton.material}
    {
        cout << "Carton copy constructor" << endl;
    }
    //3.调用基类的拷贝构造,
    Carton(string mat) : material{mat}
    {
        cout << "Carton(string) called.\n";
    }
    Carton(double side, string mat) : Box{side}, material{mat}
    {
        cout << "Carton(double,string) called.\n";
    }
    Carton() { cout << "Carton() called.\n"; }
};
int main()
{
    Carton carton{20, 30, 40, "haha"};
    Carton cartonCopy{carton}; // 这里先基类无参构造

    cout << endl;
}

using关键字,显式地“继承”基类的构造函数
实质上是将基类构造函数当做派生类的构造函数使用,初始化派生类对象的基类部分

析构:
析构派生类新增加的

5.6 友元的继承#

  1. 基类有友元类或友元函数,则其派生类不因继承 关系也有此友元类或友元函数。( B有友元)

基类的成员是某类的友元,则其作为派生类继承的成员仍是某类的友元。
(B原来能访问A的私有a,那么A有了派生类后,B还能访问其派生类中从基类继承过来的私有a)

class B;
class D;
class A{
    int a;
    friend class B;//B可以访问a,但B的儿子不能访问
};
class D:public A{
    int d;
public:
};
class B {
private:
    int b;
public:
    void get(A obja) {
        cout<<obja.a<<endl;
    }  
    void get2(D objd) {
        cout<<objd.d<<endl;//B不能访问A派生类D的私有
        cout<<objd.a<<endl;//这样是可以的
    }  
};

class C:public B {
private:
    int c;

public:
    void get(A obja) {
        cout<<obja.a<<endl;//不可以
    } 
};

5.7 菱形继承#

菱形继承概念:

​ 两个派生类继承同一个基类

​ 又有某个类同时继承者两个派生类

​ 这种继承被称为菱形继承,或者钻石继承

典型的菱形继承案例:

IMG_256

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性

  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

虚继承(virtual)就是派生类中只有一份间接基类(上面的animal)的数据

示例:

class Animal{
public:
	int m_Age;
};

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

void test01()
{
	SheepTuo st;
	st.Sheep::m_Age = 100;
	st.Tuo::m_Age = 200;

	cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
	cout << "st.Tuo::m_Age = " <<  st.Tuo::m_Age << endl;
	cout << "st.m_Age = " << st.m_Age << endl;
}


int main() {

	test01();
	return 0;
}

6 多态#

多态就是不同的东西有相同的接口,不同的东西对这个接口都有不一样的实现

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

多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

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

  • 静态多态的函数地址绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址绑定 - 运行阶段确定函数地址

函数重载 overloading——静态联编

多个不同函数使用同一个函数名,但要求这些同名函数具有不同的参数表(参数表中的参数个数不同;参数类型不同;不同类型参数的次序不同。)

函数超载(overriding) ——动态联编
许多个不同函数使用完全相同的函数名、函数参数表以及函数返回类型
基类指针可以指向其派生类的对象

虚函数:#

  1. 函数前加关键字 virtual
  2. 虚函数存在的意义是为了实现多态,让派生类能够重写(override)其基类的成员函数
  3. 虚函数是动态绑定的,在运行时才确定,而非虚函数的调用在编译时确定
  4. 虚函数必须是非静态成员函数,因为静态成员函数需要在编译时确定
  5. 构造函数不能是虚函数,因为虚函数是动态绑定的,而构造函数创建时需要确定对象类型
  6. 析构函数一般是虚函数
class Animal {  
public:  
    //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。  
    virtual void speak() {  
        cout << "动物在说话" << endl;  
    }  
};  
  
class Cat : public Animal {  
public:  
    void speak() {//重写speak  
        cout << "小猫在说话" << endl;  
    }  
};  
  
class Dog : public Animal {  
public:  
    void speak() {//重写speak  
        cout << "小狗在说话" << endl;  
    }  
};  
  
void DoSpeak(Animal &animal) {  
    animal.speak();  
}  
int main() {  
    Cat cat;  
    DoSpeak(cat);  
  
    Dog dog;  
    DoSpeak(dog);  
    //如果去掉virtual就都是动物在说话  
//经典:基类指针 指向 派生类对象
	Animal *p=new Cat();  
	p->speak();  
	delete p;
	
    return 0;  
}

纯虚函数和抽象类#

在多态中,通常基类中虚函数的实现是毫无意义的,主要都是调用派生类重写的内容
因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类就称为抽象类

抽象类特点

  • 无法实例化对象
  • 派生类必须重写抽象类中的纯虚函数,否则也属于抽象类

示例:

class Base {  
public:  
    virtual void func() = 0;  
};  
  
class Son : public Base {  
public:  
    virtual void func() {  
        cout << "func调用" << endl;  
    };  
};  
  
int main() {  
    Base *base = nullptr;  
    //base = new Base; // 错误,抽象类无法实例化对象  
    base = new Son;  
    base->func();  
    delete base;//记得销毁  
    return 0;  
}

虚析构和纯虚析构#

多态使用时,如果派生类中有属性开辟到堆区,那么基类指针在释放时无法调用到派生类的析构代码

解决方式:将基类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决基类指针释放派生类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,意味着不能直接实例化基类对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

示例:

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 != nullptr) {  
            delete m_Name;  
            m_Name = nullptr;  
        }  
    }  
  
public:  
    string *m_Name;  
};  
  
int main() {  
    Animal *animal = new Cat("Tom");  
    animal->Speak();  
  
    //通过基类指针去释放,会导致派生类对象可能清理不干净,造成内存泄漏  
    //怎么解决?给基类增加一个虚析构函数  
    //虚析构函数就是用来解决通过基类指针释放派生类对象  
    delete animal;  
    return 0;  
}

总结:

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

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

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

12 模版#

函数模版#

template <类型形参或普通形参表> 返回类型 函数模板名(函数模板形参表) {函数体}

其中,类型参数为 typename,普通参数为 int
类型形参的相应实参为类型名,而普通形参的相应实参必须为一个常量

如果函数模板与函数同名,C++编译器都将首先检查是否存在重载函数,若匹配成功则调用该函数,否则再去匹配函数模板

类模板#

template <类型形参或普通形参表> class 类模板名 ;``

类体外定义成员函数

template <typename T>
T TestClass<T> ::getData( 形参表 ) {
... //函数体
};

不能使用类模板来直接生成对象,必须先为模板参数指定“实参”, 即为模板“实例化”
类模板名 <具体的实参表> 对象名称

类模板的静态成员#

template <typename T> class CA{
static T t; //类模板的静态成员t
};

类模板的静态成员在模板定义时是不会被创建的,其创建是在类的实例化之后

13 输入输出流#

回忆之前用的

#include <cstdio>
freopen("seed.txt", "r", stdin);
freopen("seed.txt","w",stdout);

C++流类库#

c++将输入输出封装成流类

流类库的特点:

  1. 简明与可读性
  2. 类型安全(参加输入输出的数据不会被改变类型)
  3. 易于扩充

文件与流的概念#

流(Stream)
• 流是一个逻辑概念,由于输入输出都是“串行序列”
• 是C++语言对所有外部设备的逻辑抽象
• 代表的是某种流类类型的一个对象
• C++的I/O系统将每个外部设备都转换成一个称为流的逻辑设备,由流来完成对不同设备的具体操作

文件(File)
• 文件(File)是一个物理概念,代表存储着信息集合的某个外部介质, 它是C++语言对具体设备的抽象,如,磁盘文件,显示器,键盘。 又可以分为文本文件(以字节为单位)和二进制文件(以位为单位)

文件与流的联系
打开文件:将一个文件与一个流(类对象)联系起来
文件与流建立联系后,对该流(类对象)的访问就是对该文件的访问,也就是对一个具体设备的访问。
关闭文件:将一个文件与流(类对象)的联系断开。

基本流类#

  1. ios
    基本流类的虚基类,定义了IO相关的成员函数、状态标志等;
  2. istream
    ios派生,支持输入(提取“>>”)操作(运算符重载);
  3. ostream
    ios派生,支持输出(插入“<<”)操作(运算符重载);
  4. iostream
    – 由istreamostream共同派生,支持输入和输出双向操作。

预定义的流类对象(标准流)#

—— 已经写好了的,可以直接用
istream cin;
ostream cout;
通常缓冲输出,这意味着它不会立即显示在屏幕上,而是存储在缓冲区中,当缓冲区满或者遇到刷新操作(如endl)时才显示。
ostream cerr;
用于输出错误信息。不缓冲输出。写入cerr的信息会立即显示在屏幕上,合于输出那些即使程序崩溃也需要立即查看的错误信息。
ostream clog;
缓冲,标准错误流。用于不那么紧急的诊断信息。

用于磁盘操作的文件流类#

头文件:#include <fstream>
ifstream: 由istream派生,支持从磁盘文件中输入(读)数据;
ofstream: 由ostream派生,支持往磁盘文件中输出(写)数据;
fstream: 由iostream派生,支持对磁盘文件进行输入和输出数据的双 向操作。

打开文件模式:

打开文件模式 说明 文件属性 说明
ios::app 以追加的方式打开文件 0 普通文件,打开访问
ios::ate 文件打开后定位到文件尾,ios::app包含此属性
ios::binary 以二进制方式打开文件,默认是文本文件 1 只读文件
ios::in 以输入方式打开,读出文件内容
ios::out 以输出方式打开,写入文件内容 2 隐含文件
ios::nocreate 以不创建文件的方式打开文件,如果文件不存在,打开失败
ios::noreplace 以不覆盖的方式打开文件,如果文件存在,打开失败 4 系统文件
ios::trunc 如果文件存在,清空文件内容

插入与提取运算符重载#

插入运算符<<和与提取运算符>>
cout<<x;相当于 cout.operator<<(x)
cout为对象名,operator<<相当于函数名

例子:重载复数

//复数
class complex {
    double r;
    double i;

public:
    complex(double r0 = 0, double i0 = 0) {
        r = r0;
        i = i0;
    }
    complex operator+(complex c2) {
        complex c;
        c.r = r + c2.r;
        c.i = i + c2.i;
        return c;
    }
    complex operator*(complex c2) {
        complex temp;
        temp.r = (r * c2.r) - (i * c2.i);
        temp.i = (r * c2.i) + (i * c2.r);
        return temp;
    }
    friend istream &operator>>(istream &in, complex &com);
    friend ostream &operator<<(ostream &out, complex &com);
};
istream &operator>>(istream &in, complex &com) {
    in >> com.r >> com.i;
    return in;
}
ostream &operator<<(ostream &out, complex &com) {
    out << '(' << com.r << com.i << ')';
    return out;
}

输入/输出格式控制#

ios类中设置了一个long型(4字节)的数据成员用来记录当前被设置的格式状态,该数据成员被称为格式控制标志字。标志字是由若干标志位组成。
格式控制标志位是用于控制输入输出格式的选项,它们可以改变流的行为。
以下是一些常用的格式控制标志位:

  1. ios::left:输出数据在指定的宽度内左对齐。
  2. ios::right:输出数据在指定的宽度内右对齐(默认值)。
  3. ios::internal:符号位左对齐,数值右对齐。
  4. ios::dec:设置整数的基数为10。
  5. ios::oct:设置整数的基数为8。
  6. ios::hex:设置整数的基数为16。
  7. ios::showbase:在输出整数时显示基数前缀(0x或0)。
  8. ios::showpos:在正数前显示加号。
  9. ios::setw()限制输出宽度,不够的在最前面补空格。只作用于其后的一个数
  10. ios::setprecision(int n) 小数点后的保留n位
    • 当与fixed一起使用时,它指定固定小数点格式下的小数位数。
    • 当不与fixed一起使用时,它指定最大可能的位数,但不会添加额外的零
  11. ios::uppercase:在以十六进制形式输出时,使用大写字母。
  12. ios::scientific:以科学计数法格式输出浮点数。
  13. ios::fixed:以固定小数点格式输出浮点数。

格式控制标志字中设立了三组平行的标志位,程序员 应保障任何时刻只设置其中的某一个标志位

  • 表示数制标志位的ios::dec(十进制)、ios::oct(八进制)和ios::hex(十六进制)
  • 表示对齐标志位的ios::leftios::rightios::internal
  • 表示实数格式标志位的ios::scientificios::fixed
double a=3.1;
cout<<setw(4)<<a<<endl;
//也相当于cout.width(4); cout<<a;
cout<<setprecision(4)<<a<<endl;
cout<<fixed<<setprecision(4)<<a<<endl;
/*输出结果
 3.1
3.1
3.1000
*/

磁盘文件的输入与输出#

打开文件#

    ifstream fin("1.txt");
    // 等价于 
    ifstream fin;
    fin.open("1.txt");

文件流类的构造函数
ifstream ::ifstream( const char* 文件名, int nMode = ios::in, nProt = filebuf::openprot );
ofstream::ofstream( const char* 文件名, int nMode = ios::out, int nProt = filebuf::openprot );
nMode -- 打开文件的方式,也称访问模式。
• ios::in -- 以读(输入)为目的打开,默认参数。
• ios::trunc -- 若文件存在,则清除其内容。
• ios::nocreate -- 仅打开一个已存在文件,如果文件不存在,则操作失败。
• ios::binary -- 打开二进制文件,缺省为文本文件。

fstream类由iostream所派生

文件流类的成员函数#

  1. get()put()
    • 读写字符
    • 多用于读写文本文件
    get(char &ch)
    put(char ch)
    //输入字符串,将其写入文件,再从文件读入输出到屏幕
    char s[10];
    cin.getline(s,10);
    ofstream file("1.txt");
    for(int i=0;s[i];i++)
        file.put(s[i]);
    file.close();
    ifstream fin("1.txt");
    char ch;
    fin.get(ch);
    while(!fin.eof()) {//eof()判断是否读到文件末尾
        cout<<ch;
        fin.get(ch);
    }
  1. read()write()
    • 读写二进制数据
    • 多用于读写二进制文件

read(char *ch,int cnt)
write(const char *ch,int cnt)

#include <fstream>
#include <iostream>
using namespace std;
int main() {

    //打开用于“写”的二进制磁盘文件
    ofstream fout("wrt_read_file.bin", ios::binary);
    char str[20] = "Hello world!";
    int len1 = strlen(str);
    fout.write((char *) (&len1), sizeof(int));//注意是len1的地址
    //不能传len1的数值因为write函数不知道如何解释这个值。它需要一个指针来知道从哪里开始读取数据,
    //以及需要读取多少数据。通过传递len1的地址并指定写入sizeof(int)字节,
    //告诉write函数从len1所在的内存位置开始,写入int类型大小的数据。
    fout.write(str, sizeof(str));
    fout.close();
    cout << "-- READ it from 'wrt_read_file.bin' -- " << endl;
    char str2[20];
    ifstream fin("wrt_read_file.bin", ios::binary);
    int len2;
    // fin.read(str2, len);
    fin.read((char *) (&len2), sizeof(int));
    str2[len2] = '\0';//加串尾符
    fin.read(str2, len2);
    cout << "Len = " << len2 << endl;
    cout << "str2 =" << str2 << endl;
    fin.close();

    return 0;
}
  1. getline()
    • 读字符串
    • 多用于读文本文件
    getline(char *ch,int cnt,char delim='\n')
    从某个文件中读出一行(至多cnt个字符) 放入ch缓冲区中,默认行结束符为'\n'

文本文件与二进制文件#

文本文件(.txt)
以text形式存储

  • 优点是具有较高的兼容性
  • 缺点是存储数值信息时,存储的是数值对应的文本即 字符串,要在数据之间人为地添加分割符,否则将导致数据无法正确读出
  • 输入输出过程中,系统要对存 储的数据格式进行相应转换
    二进制文件(.bin)
    以binary形式存储
  • 优点是便于对数据实行随机访问(每一同类型数据所 占磁盘空间的大小均相同,不必在数据之间人为地添 加分割符)
  • 缺点是兼容性低,不可利用文本编辑器进行编辑阅读
  • 输入输出过程中,系统不对数据进行任何转换,IO效率高

默认打开方式为text文件形式

对数据文件进行随机访问

ostream& seekp( long off, ios::beg );
将“输出指针”的值移到一个新位置,新位置由参数off与dir之值确定:

  • 当dir之值为“ios::beg”时,新位置为:从文件首“后推”off字节处;
  • 当dir之值为“ios::cur”时,新位置为:从“输出指针”的当前位置“后推”off字节处;
  • 当dir之值为“ios::end”时,新位置为:从文件末“前推”off字节处。
    istream& seekg( long off, ios::beg );
    读入指针

long tellp( );
获取“输出指针”的当前位置值。

long tellg( );
获取“读入指针”的当前位置值。

补充#

类模板#

除了定义的时候可以单独出现,其他都必须后面跟一个<T>

#include <iostream>  
  
using namespace std;  
  
template<class T>  
class Person{  
public:  
    Person(T aa,T bb):a(aa),b(bb) {}  
    void show() {  
        cout<<"a = "<<a<<",b = "<<b<<endl;  
    }  
private:  
    T a;  
    T b;  
};  
int main () {  
    Person<int> p(10,20);  //关键
    p.show();  
    return 0;  
}
template <int i>
class Testclass{
public:
	int buffer[i];
	//使ouffer的大小可变化,但其类型则固定为int
	int getData(int j);
};
  1. 精确匹配:如果存在一个非模板函数,其参数类型与调用时的实参完全匹配,则优先选择这个非模板函数。

  2. 模板实例化:如果没有任何非模板函数与实参精确匹配,但有一个函数模板可以实例化为与实参匹配的版本,则选择这个模板实例。

#include <iostream>

void func(int x) {
    std::cout << "Calling non-template function with int: " << x << std::endl;
}

template<typename T>
void func(T x) {
    std::cout << "Calling template function with T: " << x << std::endl;
}

int main() {
    func(42);     // 调用非模板函数
    func<>(42);   // 显示调用模板函数
    func('a');    // 调用模板函数,因为没有匹配的非模板函数接受char参数
    return 0;
}

`

作者:AuroraKelsey

出处:https://www.cnblogs.com/AuroraKelsey/p/18669379

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   AuroraKelsey  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示