C++笔记

一、C++基础入门

1、变量

#include<iostream>

using namespace std;

int main()
{
	/*
	变量创建的语法:
		数据类型 变量名 = 变量初始值;
	*/
	int a = 18;

	cout << "C++的变量类型,a = " << a << endl;
	system("pause");
	return 0;
}

2、常量

1、#define定义宏常量
#define Week 7
2、const 修饰
const int a = 23;

3、关键字

4、标识符命名规则

C++标识符(变量、常量)命名规则

  • 标识符不能是关键字
  • 标识符只能由字母、数字、下划线组成
  • 第一个字符必须为字母或下划线
  • 标识符中字母区分大小写

5、C++输入

cin<< 输入

#include<iostream>

using namespace std;

int main()
{
	// cin>> :从键盘输入数据

	int a = 0;
	cout << "请输入a的值" << endl;
	cin >> a;
	cout << "a = " << a << endl;

	//输入字符串
	string str;
	cout << "请输入字符串的值" << endl;
	cin >> str;
	cout << "str = " << str << endl;

    //接受键盘输入的字符型数据
	float f1;
	cout << "请输入浮点型数字" << endl;
	cin >> f1;
	cout << "f1 = " << f1 << endl;

	system("pause");
	return 0;
}

二、指针

1、const修饰指针

const修饰指针有三种情况:

  • const修饰指针 — 常量指针
int a = 10;
int b = 10;
const int *p = &a; //在指针前面加const修饰

p = &b; //正确,指向可以修改
*p = 20; //错误,const修饰的是指针,所以取*的操作不允许。指针指向的值不可以修改

常量指针特点:指针的指向可以修改,但指针指向的值不可以更改。

  • const修饰常量 — 指针常量
int a = 10;
int b = 10;
int * const p = &a; //指针常量(const修饰p,前面int*表示是一个指针)

p = &b;//const修饰变量,所以p不能被修改。错误,指针的指向不可以修改
*p = 20; //正确

指针常量特点:指针的指向不可以修改,但指针指向的值可以更改。

  • const既修饰指针、又修饰量
int a = 10;
int b = 10;

const int* const p = &a; //const既修饰指针、又修饰常量

特点:指针的指向和指针指向的值都不可以修改。

三、内存结构

1、C语言运行之前

1、预处理:宏定义展开、头文件展开、条件编译,不会检查语法

2、编译:检查语法,将预处理文件编译生成汇编文件

3、汇编:将汇编文件生成目标文件(二进制文件)

4、链接:将目标文件链接为可执行程序

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

  • **代码区:**存放CPU执行的机器指令、共享的、只读的

  • 全局区:全局变量、静态变量;常量区(包括字符串常量和其他常量)

总结:

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

2、运行之后

2.1、栈区
  • 由编译器自动分配释放、存放函数的参数值、局部变量
  • 注意:不要返回局部变量的地址。因为栈区开辟的数据由编译器自动释放
int fun(int b){ //形参数据也放在栈区
    int a = 10;
    return &a; //返回局部变量的地址
}

void main()
{
    int b = 4;
    int *p = fun(b);
    cout<<*p<<endl; //打印10,正确。这是因为编译器做了保留
    cout<<*p<<endl; //打印输出错误,第二次这个数据就被销毁了。
}
2.2、堆区
  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • 手动释放利用操作符delete
  • 在C++中,主要利用new在堆区开辟
**类型***作用域***生命周期***存储位置*
auto变量一对{}内当前函数栈区
static局部变量一对{}内整个程序运行期初始化在data段,未初始化则在BSS段
extern变量整个程序整个程序运行期初始化在data段,未初始化则在BSS段
static全局变量当前文件整个程序运行期初始化在data段,未初始化则在BSS段
extern函数整个程序整个程序运行期代码区
static函数当前文件整个程序运行期代码区
register变量一对{}内当前函数运行时存储在CPU寄存器
字符串常量当前文件整个程序运行期data段

3、new关键字

#include<iostream>
using namespace std;

//在堆上生成数据
int* fun1()
{
	int* p = new int(10);
	return p;
}

//2 在堆上利用new 开辟数组
void test02()
{
	//创建数组
	int * arr = new int[10];

	for (int i = 0; i < 10; i++)
	{
		arr[i] = i + 1;
	}

	for (int i = 0; i < 10; i++)
	{
		cout << arr[i]<<", ";
	}

	//释放数组,需要加一个中括号 " [] "
	delete[] arr;

}

void main()
{
	int* p = fun1();

	cout << *p << endl;
	cout << *p << endl;
	cout << *p << endl;
	delete p; //清除堆上面的内存
	
    test02();
}

4、易混淆概念

代码区:存放程序编译后的二进制代码,不可寻址区

数据区包括堆,栈,全局/静态存储区

全局/静态存储区包括:全局区(extent),静态区(static),常量区(const)

常量区包括:字符串常量、常变量(const)

5、引用

给变量起别名

1、语法格式:

数据类型 &别名 = 原名

2、引用注意点
  • 引用必须要初始化; int &b; //错误,没有初始化
  • 引用一旦初始化之后,就不可以更改了
3、引用做函数返回值
  • 1、不要返回局部变量的引用
  • 2、函数的调用可以作为左值
#include<iostream>
using namespace std;

//引用做函数的返回值
//1、不要返回局部变量的引用
int& test01()
{
	int a = 10; //局部变量在栈区'
	return a; //返回局部变量
}

//2、函数的调用可以作为左值
int& test02() {
	static int a = 10; //静态变量,存放在全局区。全局区的数据在程序结束后系统释放
	return a;
}

int main()
{
	int& ref = test01();
	cout << "ref = " << ref << endl; //第一次时打印正确,因为编译器做了保存
	cout << "ref = " << ref << endl; //第二次打印错误
	 
	int& ref2 = test02();
	cout << "\nref2 = " << ref2 << endl;
	
	test02() = 100;  //相当于:a = 100; 如果函数的返回值是引用,那么这个函数调用可以作为左值

	cout << "\nref2 = " << ref2 << endl; 
	system("pause");
	return 0;
}
4、引用的本质

引用本质在C++内部实现是一个指针常量

  • 指针常量(const修饰变量名):指针的指向不可以改变,指针指向的值可以改变
#include<iostream>
using namespace std;

int main()
{
	//引用的本质是一个指针常量
	int a = 10;
	
	//自动转换为 int* const ref = &a; 指针常量:指针的指向不可以改变,但指向的值可以改变
	int& ref = a;
	ref = 20; //内部发现是一个引用,则自动转换为:*ref = 20;

	cout << "a = " << a << endl;
	cout << "ref = " << ref << endl; //自动转换为 *ref

	system("pause");
	return 0;
}

指针常量练习:

#include<iostream>
using namespace std;

int main()
{
	//引用的本质是一个指针常量
	int a = 10;
	int b = 20;
	int* const p = &a; //指针常量:指针的指向不可以改变,但是指针指向的值可以改变

	//p = &b; //非法,指针常量的指向不可以改变(const修饰哪一个,哪一个就不可以改变)
	*p = b; //指针指向的值可以改变
	cout << "*p = " << *p <<", a = "<< a << endl;

	system("pause");
	return 0;
}
5、常量引用

作用:主要用来修饰形参,防止误操作。

#include<iostream>
using namespace std;

void print(const int & value)
{
	//value = -1; //加入const之后只读,不能修改
	cout << "value = " << value << endl;
}
int main()
{
	int a = 1000;
	int& ref = a;

	//引用必须引一块合法的内存空间
	//const int& ref2 = 10;实际上编译器做了如下操作:int temp = 10; const int& ref2 = temp;
	const int& ref2 = 10; //加入const之后只读,不能修改
	cout << "ref2 = " << ref2 << endl;

	print(a);
	cout << "a = " << a << endl;
	system("pause");
	return 0;
}

三、函数提高

1、形参默认值、占位符
void fun(int a,int b = 10, int c = 20){ //默认值
    
}
void fun(int a,int){ //第二个参数为占位参数,调用此函数时必须赋值
    
}

2、函数重载

2.1、概述
  • 函数名相同,提高复用性

重载条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同,或者个数不同、顺序不同

注意:函数返回值不可以作为函数重载的条件

2.2、函数重载注意事项
  • 引用作为重载条件
#include<iostream>
using namespace std;

void func(int& a)
{
	cout << "int& a引用参数函数" << endl;
}

void func(const int& a) {
	cout << "const int& a引用作为函数参数" << endl;
}

int main02()
{
	int a = 10;
	func(a); //传入参数是变量,所以调用func(int& a)。输出 int& a引用参数函数

	const int b = 10;
	func(b); //传入常量,所以调用func(const int& a)。输出: const int& a引用作为函数参数

	system("pause");
	return 0;
}
  • 函数重载碰到函数默认值:注意出现二义性

四、类和对象

4.1 封装

4.1.1 封装的意义

意义一:

意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制。

访问权限:

public:任何地方都可以访问(类内和类外)

protected:类外不可以访问

private:类外不可以访问

4.1.2 struct和class的区别

struct默认权限为公共public;class默认权限为私有 private

4.1.3 成员属性私有化

优点1:将所有成员属性设置为私有,可以控制读写权限

优点2:对于写权限,可以检测数据的有效性

案例

#include<iostream>
using namespace std;

//定义一个立方体类
class Cube{
private:
	float m_L; //长
	float m_H; //高
	float m_W; //宽

public:
	//get set方法
	void setCube(float L, float W, float H){
		m_L = L;
		m_W = W;
		m_H = H;
	}
	float* getCube(){
		float cubeSide[] = {m_L,m_W,m_H};
		return cubeSide;
	}

	//计算边长
	float calculateSide(){
		float res = (m_L + m_W + m_H) * 2;
		return res;
	}

	float calculateArea(){
		float area = m_L * m_W * m_H;
		return area;
	}
};

int main()
{
	Cube cube1;
	//设置第一个立方体的边长
	cube1.setCube(2.4,3,4);

	Cube cube2;
	cube2.setCube(3,5,1.6);

	cout<<"cube1的周长为:"<<cube1.calculateSide()<<endl;
	cout<<"cube1的面积为:"<<cube1.calculateArea()<<endl;

	cout<<"cube2的周长为:"<<cube2.calculateSide()<<endl;
	cout<<"cube2的面积为:"<<cube2.calculateArea()<<endl;

	system("pause");
	return 0;
}

4.2 对象的初始化和清理

4.2.1 构造函数和析构函数
  • 构造函数:主要作用于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
  • 析构函数:主要作用于对象销毁前系统自动调用,执行一些清理工作

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

  1. 构造函数,没有返回值也不写void
  2. 函数名与类名相同
  3. 可以有参数,因此可以发生重载
  4. 自动调用,而且只会调用一次

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

  1. 没有返回值也不写void
  2. 函数名与类名相同,在名称前加上符号~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前自动调用析构函数,无须手动调用,而且只调用一次
#include<iostream>
using namespace std;

class Person{
public:
	Person(){
		cout<<"构造函数调用"<<endl;
	}

    //析构函数
	~Person(){ //自动调用,不能有参数
		cout<<"析构函数调用"<<endl;
	}
};

void test01(){
	Person p1; //创建对象
}

int main()
{
	test01();
	system("pause");
	return 0;
}
4.2.2 构造函数的分类及调用
  • 按参数分为:有参构造、无参构造

  • 按类型分为:普通构造、拷贝构造

//拷贝构造函数
	Person(const Person &p){
		//将传入的参数身上的所有属性,都拷贝到自己身上
		age = p.age; 
	}
  • 按调用方式分:括号法、显示法、隐士转换法
#include<iostream>
using namespace std;

class Person{
public:
	Person(){
		cout<<"无参构造函数调用"<<endl;
	}
	Person(int a){
		age = a;
		cout<<"有参构造函数调用"<<endl;
	}

	~Person(){ //自动调用
		cout<<"析构函数调用"<<endl;
	}

	//拷贝构造函数
	Person(const Person &p){
		//将传入的人身上的所有属性,都拷贝到我身上
		cout<<"拷贝构造函数调用"<<endl;
		age = p.age; 
	}

private:
	int age;
};

void test01(){
	//注意事项1:使用默认的无参构造时,不能加().因为会认为是一个函数声明
	Person p1; //创建对象

	//按调用分类:括号法、显示法、隐士转换法
	//不能使用括号法调用无参构造
	//Person p2(10);

	//2、显示法
	Person p3 = Person(p1);

	Person(20); //匿名对象。当前执行完成,立即被回收(调用析构函数)
	cout<<"aaa"<<endl;

	//注意事项2:不要利用拷贝构造函数,初始化匿名对象。
	//编译器会认为 Person(p3) == Person p3; //重定义错误

	//3、隐士转换法
	Person p4 = 10; //相当于 Person p4 = Person(10);
	Person p5 = p4; //拷贝构造函数
}

int main()
{
	test01();
	system("pause");
	return 0;
}

注意事项1:使用默认的无参构造时,不能加(). 因为会认为是一个函数声明

注意事项2:不要利用拷贝构造函数,初始化匿名对象。

4.2.3 拷贝构造函数调用时机
1、使用一个已经创建完毕的对象来初始化一个新对象
2、值传递的方式给函数参数传值
void doWork(Person p1){
    //值传递的方式给函数参数传值,会调用拷贝构造函数复制一份相同的对象,在此函数中对p1进行修改,不会影响调用函数中的实参对象
}
3、值的方式返回局部对象
Person doWork2(){
    Person p1;
    return p1; //返回局部对象会调用拷贝构造函数复制出一个相同对象,原始的p1在栈中,函数结束时就会被销毁返回
}
4.2.4 构造函数调用规则

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

  • 1、默认构造函数(无参、函数体为空)
  • 2、默认析构函数(无参、函数体为空)
  • 3、默认拷贝构造函数,对属性进行值拷贝

调用规则

  • 如果用户定义了有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造函数
  • 如果用户定义了拷贝构造函数,C++不再提供其他构造函数
4.2.5 深拷贝和浅拷贝

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

#include<iostream>
using namespace std;

class Person {
public:
	Person() {
		cout << "无参构造函数" << endl;
	}

	Person(int a,float height) {
		m_Age = a;
		m_Hight = new float(height);
		cout << "有参构造函数" << endl;
	}
	~Person() {
		//析构函数,将堆区开辟的数据做释放操作
		
		if (m_Hight != NULL) {
			delete m_Hight;
			m_Hight = NULL;
		}

		cout << "析构函数" << endl;
	}
	Person(const Person& p) {
		cout << "拷贝构造函数" << endl;
		m_Age = p.m_Age; //浅拷贝
		//浅拷贝 m_Hight = p.m_Hight; 在堆区的数据存在重复释放内存的问题
		//这里进行深拷贝操作,解决上述问题
		m_Hight = new float(*p.m_Hight);
	}
	int m_Age;
	float* m_Hight; //身高指针
};

void test01()
{
	Person p1(27, 1.67);
	cout << "p1的年龄为: " <<p1.m_Age  <<"	身高为:"<<*p1.m_Hight<< endl;

	Person p2(p1);
	cout << "p2的年龄为: " << p2.m_Age << "	身高为:" << *p2.m_Hight << endl;
}

int main()
{
	test01();
	system("pause");
	return 0;

}

总结:如果属性有在堆区开辟的,一定要程序员提供拷贝构造函数,防止浅拷贝带来的问题。

4.2.6 初始化列表

作用:C++提供了初始化列表语法,初始化属性

语法:构造函数(): 属性1(值1), 属性2(值2),…{}

class Person {
public:
	//初始化列表
	Person(int a, int b, int c) : m_A(a), m_C(c), m_B(b) {}

		int m_A, m_B, m_C;
};
4.2.7 类对象作为类成员

构造顺序:先构造成员对象,在构造当前类。析构顺序与构造相反

4.2.8 静态成员

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

分类:

  • 静态成员变量
    • 所有对象共享一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
#include<iostream>
using namespace std;

class Person {
public:
	static void func() {
		m_Age = 10;
		//m_Hight = 178; 静态成员函数不能访问非静态成员变量。因为无法区分该变量是属于谁的

		cout << "static void func调用" << endl;
	}

	static int m_Age;
	int m_Hight;
};

void test04_01()
{
	//1、通过对象访问
	Person p;
	p.func(); 

	//2、通过类名直接访问
	Person::func(); 

}

int main()
{
	test04_01();
	system("pause");
	return 0;
}

4.3 C++对象模型和this指针

4.3.1 成员变量和成员函数分开存储

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

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

#include<iostream>
using namespace std;

class Person {

	int m_A; //非静态成员变量,属于类的对象上的

	static int m_B; //静态成员变量,不属于类的对象上

	void func() //非静态成员函数,不属于类的对象上
	{

	}

	static void func2()//静态成员函数,不属于类的对象上
	{

	}

};
int Person::m_B = 1; //静态成员变量,需要在类外赋初值

void test01() {
	Person p;
	
	//空对象占用内存空间为:1
	//C++编译器会给每个空对象分配一个字节空间,为了区分空对象占内存的位置。
	//每个空对象都有一个独一无二的内存地址
	cout << "空对象所在字节数为:" << sizeof(p) << endl;
}

void test02() {
	Person p;
	cout << "sizeof(p) = " << sizeof(p) << endl;
}
int main() {

	test02();
	system("pause");
	return 0;
}
4.3.2 this指针

C++中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。this指针用于区分对象调用自己的代码。

this指针本质上是一个指针常量:指针的指向不可以修改

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

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

this指针的用途

  • 形参和成员变量同名时,可以用this指针来区分
Person(int m_A) {
    //this指向被调用的成员函数所属的对象
    this->m_A = m_A;
}
  • 在类的非静态成员函数中返回对象本身,可使用return *this.
Person& addPersonAge(Person &p) { //返回的是一个引用,否则(拷贝构造函数)将采用浅拷贝方式返回一个新的对象
    this->m_A += p.m_A;
    return *this; //*this是Person
}
4.3.3 空指针访问成员函数

空指针也可以调用成员函数,但是要注意有没有用到this指针。如果用到this指针,需要加以判断保证代码的健壮性。

4.3.4 const修饰成员函数

常函数

  • 在成员函数后面加const修饰的函数称为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改
class Person {
public:
	/*
		this指针的本质是 指针常量 ,所以this指针的指向不可以修改
		const Person * const this
		在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
	*/
	void showPerson() const
	{
		// this->age = 10; 不可以被修改
		this->height = 170;
	}

	int age;
	mutable int height; //特殊变量,mutable修饰的变量可以被常函数修改
};

常对象

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
#include<iostream>
using namespace std;

class Person {
public:
	/*
		this指针的本质是 指针常量 ,所以this指针的指向不可以修改
		const Person * const this
		在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
	*/
	void showPerson() const
	{
		// this->age = 10; 不可以被修改
		this->height = 170;
	}

	int age;
	mutable int height; //特殊变量,mutable修饰的变量可以被常函数修改
};


int main()
{
	const Person p;
	//p.age = 10; //不能修改
	p.height = 189; //常对象可以修改mutable修饰的变量
	p.showPerson(); //常对象只能调用常函数

	system("pause");
	return 0;
}

4.4 友元

友元的关键字:friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
4.4.1 全局函数做友元
class Building
{
	friend void test01_01(); //声明该全局函数为友元
public:
	Building() {
		m_sittingRoot = "客厅";
		m_bedRoot = "卧室";
	}
	string m_sittingRoot;//客厅

private:
	string m_bedRoot; //卧室
};
//全局函数
void test01_01() {
	Building bu;
	bu.m_sittingRoot;
	cout << "友元全局函数正在访问:" << bu.m_sittingRoot << endl;
	cout << "友元全局函数正在访问:" << bu.m_bedRoot<< endl;
}
4.4.2 类做友元
class Building
{
	friend class goodGay; //声明类goodGay为友元
public:
	Building() {
		m_sittingRoot = "客厅";
		m_bedRoot = "卧室";
	}
	string m_sittingRoot;//客厅

private:
	string m_bedRoot; //卧室
};

class goodGay{
    
}
4.4.3 成员函数做友元
class Building
{
	friend void goodGay::visit(); //声明成员函数visit为友元
public:
	Building() {
		m_sittingRoot = "客厅";
		m_bedRoot = "卧室";
	}
	string m_sittingRoot;//客厅

private:
	string m_bedRoot; //卧室
};

class goodGay{
public:
    void visit(){
        cout<<"好基友正在访问"<<endl;
    }
    
}

4.5 运算符重载

运算符重载有两种方式:1)成员函数重载; 2)全局函数重载

4.5.1 加号重载
#include<iostream>
using namespace std;

class Person
{
    friend Person operator+(Person& p1, Person& p2); //友元函数
    friend ostream& operator<<(ostream& cout, Person& p); //友元函数
public:
	Person() {
		m_A = 10;
		m_B = 20;
	}
    //2. 加号的成员函数重载
	Person operator+(Person& p) {
		Person temp;
		temp.m_A = p.m_A + this->m_A;
		temp.m_B = p.m_B + this->m_B;
		return temp;
	}
private:
	int m_A;
	int m_B;
};

//2. 加号的全局函数重载
Person operator+(Person& p1, Person& p2) {
	Person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}

//3. << 运算符重载:必须是全局函数
ostream& operator<<(ostream & cout,Person &p) {
	cout << "m_A = " << p.m_A << ", m_B = " << p.m_B;
	return cout;
}


//测试
void test01()
{
	Person p1;
	Person p2;
	Person p3 = p1 + p2;
	cout << p1 <<endl; //注意这儿输出使用到了 << 重载
	cout << p3 << endl;
}

int main()
{
	test01();
	system("pause");
	return 0;
}
4.5.2 << 运算符重载
#include<iostream>
using namespace std;

class Cat {
	friend ostream& operator<<(ostream& cout, Cat& cat);//友元函数
public:
	Cat() {
		name = "小黑";
		color = "White";
		age = 3;
	}

private:
	string name;
	string color;
	int age;
};

// << 运算符重载,注意返回值,形参列表。<<只能是全局函数重载
ostream& operator<<(ostream& cout, Cat& cat)
{
	cout << "name:" << cat.name << "   age:" << cat.age << "   color:" << cat.color;
	return cout;
}

int main()
{
	Cat cat;
	cout << cat << endl;
	system("pause");
	return 0;
}
4.5.3 ++ 运算符重载

注意点:

  • 1、前置自增运算符重载,返回引用
  • 2、后置自增运算符重载,返回的是对象(值)
  • 3、后置自增运算符重载:int 占位参数。形参列表使用int与前置区分,而且只能使用int, double float不好使
    • 后置递增重载返回的是值,而不是返回引用。因为temp是局部变量,局部变量结束时该变量就被销毁了
  • 4、在后置递增使用<< 链式输出时,对于<<的运算符重载的形参为 ostream& operator<<(ostream& cout, MyInteger p),即第二个参数非引用传递。因为在后置递增操作中,返回的是新生成的一个对象temp, 而不是原来的对象。因此,是对temp做链式操作。
#include<iostream>
using namespace std;

class MyInteger
{
	friend ostream& operator<<(ostream& cout, MyInteger p);

public:
	MyInteger()
	{
		m_Num = 0;
	}

	MyInteger(int num) {
		m_Num = num;
	}

	//前置自增运算符重载,返回引用
	MyInteger& operator++() {
		m_Num++;
		return *this; //返回自身 *this
	}
	MyInteger operator++(int)
	{
		//先记录当时的结果
		MyInteger temp = *this;
		m_Num++;
		return temp;
	}

private:
	int m_Num;
};

//注意:这里<<运算符重载的 MyInteger p 是值传递,不能传递引用,因为在后置递增操作中,返回的是
//temp,而不是原来的对象。因此,是对temp做链式操作。
ostream& operator<<(ostream& cout, MyInteger p)
{
	cout << "m_Num:" << p.m_Num;
	return cout;
}

void test301()
{
	MyInteger p1(0);
	//前置递增
	cout << ++(++p1) << endl;
	cout << p1 << endl;
}

void test302()
{
	//后置递增
	MyInteger p2(0);
	cout << p2++ << endl;
	cout << p2 << endl;
}

int main()
{
	test302();

	system("pause");
	return 0;
}
4.5.4 赋值运算符重载

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

  • 1、默认构造函数(无参、函数体为空)
  • 2、默认析构函数(无参、函数体为空)
  • 3、默认拷贝构造函数,对属性值进行拷贝
  • 4、赋值运算符operator=,对属性进行值拷贝

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3ppf1lNu-1617755828320)(C:\Users\李柏松\AppData\Roaming\Typora\typora-user-images\image-20201127081456103.png)]

编译器提供的是浅拷贝,所以赋值操作将使p1和p2指向同一块内存。导致在调用析构函数时,重复释放同一块内存,从而报错。因此,需要对=进行重载,深拷贝解决。

#include<iostream>
using namespace std;

class Person04
{
public:
	//构造函数,在堆上开辟空间,需要程序员手动申请内存,释放内存
	Person04(int age) {
		m_age = new int(age);
	}

	~Person04() {
		//如果不为空,就释放
		if (m_age != NULL) {
			delete(m_age);
			m_age = NULL;
		}
	}

    //赋值运算符重载
	Person04& operator=(Person04& p)
	{
		//编译器提供的是先拷贝
		//m_age = p.m_age;
		//应该首先判断是否有属性在堆区,如果有先释放干净
		if (m_age != NULL) {
			delete m_age;
			m_age = NULL;
		}
		//深拷贝,重新申请一个
		m_age = new int(*p.m_age);
		return *this;
	}

	int *m_age;
};

void test04_01() {
	Person04 p1(10);
	Person04 p2(20);
	Person04 p3(30);
	//因为编译器的=是浅拷贝,下属的赋值操作将使p1和p2指向同一块内存。
	//导致在调用析构函数时,重复释放同一块内存,从而报错。因此,需要对=进行重载,深拷贝解决
	p3 = p2 = p1;

	cout <<"p1的年龄为:"<< *p1.m_age << endl;
	cout << "p2的年龄为:" << *p2.m_age << endl;
	cout << "p3的年龄为:" << *p3.m_age << endl;
}
4.5.5 关系运算符重载
class Dog
{
public:
	Dog(int age) {
		m_Age = age;
	}

	//比较运算符重载
	bool operator==(Dog& d) {
		if (m_Age == d.m_Age)
			return true;
		else
			return false;
	}

	int m_Age;
};
4.5.6 函数调用运算符重载
  • 函数调用符 () 也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此成为仿函数
  • 仿函数没有固定写法,非常灵活
#include<iostream>
using namespace std;
class Person05
{
public:
	//函数调用符() 重载
	void operator()(string str) {
		cout << str << endl;
	}
};

int main()
{
	Person05 p;
	p("hello world");

	system("pause");
	return 0;
}

4.6 继承

4.6.1 语法

class 子类(派生类): 继承方式 父类(基类)

4.6.2 继承方式
  • public继承
  • protected继承
  • private继承

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U7lH5tvq-1617755828322)(C:\Users\李柏松\AppData\Roaming\Typora\typora-user-images\image-20201127100049782.png)]

4.6.3 继承中的对象模型

父类中所有非静态成员属性都会被子类继承

父类中的私有属性的成员,是被编译器隐藏了。

**利用开发人员工具查看对象模型:**

  • 在 visual studio 文件夹中打开 Development Command Prompt for VS 2019 工具;
  • 切换到该cpp文件所在的文件夹
  • 输入 cl /d1 reportSingleClassLayout类名 文件名.cpp
    • 查看01-继承中的对象类型.cpp文件中的Son类: cl /d1 reportSingleClassLayoutSon 01-继承中的对象类型.cpp
01-继承中的对象类型.cpp

class Son1      size(16):
        +---
 0      | +--- (base class Person)
 0      | | m_A
 4      | | m_B
 8      | | m_C
        | +---
12      | son_A
        +---
#include<iostream>
using namespace std;
class Person {
public:
	int m_A;

protected:
	int m_B;

private:
	int m_C;
};

class Son1 :public Person {
public:
	int son_A;
};

int main()
{
	cout << "sizeof Son = " << sizeof(Son1) << endl; //输出16
	system("pause");
	return 0;
}
4.6.4 继承中构造和析构的顺序

执行顺序:父类构造—子类构造—子类析构—父类析构

#include<iostream>
using namespace std;

class Base
{
public:
	Base() {
		cout << "父类构造函数调用~" << endl;
	}

	~Base() {
		cout << "父类析构函数调用~" << endl;
	}
};

class Son :public Base
{
public:
	Son() {
		cout << "子类构造函数调用" << endl;
	}
	~Son() {
		cout << "子类析构函数调用" << endl;
	}
};

void test01()
{
	//Base b1;
	Son son1;
}

int main()
{
	test01();
	system("pause");
	return 0;
}
4.6.5 继承中父类子类成员同名处理
  • 子类可以直接访问子类的同名的成员
  • 子类和父类同名成员时,访问父类的成员需要加作用域符号 ::
  • 如果子类和父类有同名函数时,子类会隐藏父类中所有的同名成员函数(包括重载的成员函数),因此访问父类成员函数需要加作用域 ::
#include<iostream>
using namespace std;

class Father
{
public:
	Father() {
		m_A = 100;
	}
	int m_A;
};

class Son2 :public Father
{
public:
	Son2() {
		m_A = 200;
	}
	int m_A;
};
void test03_01()
{
	Son2 s;
	cout << "Son下的m_A = " << s.m_A << endl;
	cout << "Son下的m_A = " << s.Father::m_A << endl;
}
int main()
{
	test03_01();

	system("pause");
	return 0;
}
4.6.6 继承同名静态成员的处理方式

静态成员变量特点:

  • 1、所有对象都共享同一份数据
  • 2、编译阶段就分配内存
  • 3、类内声明,类外初始化

静态成员函数特点:

  • 1、所有对象共享同一个函数实例
  • 2、只能访问静态成员变量,不能访问非静态成员变量

与非静态成员处理方式一样:访问父类加作用域 :: ,访问自己直接访问

#include<iostream>
using namespace std;

class father04
{
public:
	static int m_A; //声明静态变量

	static void func() {
		cout << "父类静态函数调用" << endl;
	}
};
//类外初始化静态变量
int father04::m_A = 100;

class son04:public father04
{
public:
	//类内声明静态变量
	static int m_A;

	static void func() {
		cout << "子类静态函数调用" << endl;
	}

};
//类外初始化静态变量
int son04::m_A = 200;

void test04_01()
{
	son04 s;
	cout << "son(m_A) = " << s.m_A << endl;
	cout << "father(m_A) = " << s.father04::m_A << endl;

	cout << "不创建对象,直接通过类名访问" << endl;
	cout << "son(m_A) = " << son04::m_A<< endl;
	cout << "father(m_A) = " << son04::father04::m_A<< endl;


	son04::func(); //访问子类自己的静态成员函数
	son04::father04::func();//访问父类的静态成员函数
}

int main()
{
	test04_01();

	system("pause");
	return 0;
}

4.7 多态

4.7.1 多态的基本概念

C++多态分为两类:

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

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

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

动态多态条件:

  • 1、存在继承关系
  • 2、子类要重写父类的虚函数
  • 3、父类的指针或引用指向子类型对象(多态使用方法)
#include<iostream>
using namespace std;

class Animal
{
public:
	//virtual 虚函数
	virtual void speak() {
		cout << "动物在说话" << endl;
	}
};
class Cat : public Animal
{
public:
	void speak()
	{
		cout << "小猫咪在说话:喵喵喵~" << endl;
	}
};

/*
	执行说话的函数
	地制早绑定,在编译阶段就确定了函数地址。调用此函数会执行动物类的speak方法
	如果想执行cat类的speak方法,那么这个函数地址就不能提前绑定,需要在运行阶段绑定。地址晚绑定
*/
void doSpeak(Animal & animal)  //等价于:Animal & animal = cat;
{
	animal.speak();
}

void test05_01()
{
	Cat cat;
	doSpeak(cat);
}

int main()
{
	test05_01();

	system("pause");
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNEWsbCC-1617755828325)(E:\C++相关\图\多态底层原理.jpg)]

4.7.2 纯虚函数和抽象类

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

当类中有了纯虚函数,这个类就叫做抽象类

抽象类特点:

  • 1、抽象类无法实例化对象
  • 2、子类必须重写抽象类中的纯虚函数,否则也是抽象类
#include<iostream>
using namespace std;
class Birds
{
public:
	//纯虚函数。有纯虚函数的类成为抽象类,抽象类无法实例化对象
	virtual void run() = 0;
};

class check :public Birds{
public:
	void run()
	{
		cout << "小鸡在跑" << endl;
	}
};

void test06_01()
{
	Birds* bird = new check;
	bird->run();
}

int main()
{
	test06_01();
	system("pause");
	return 0;
}
4.7.3 虚析构和纯虚析构函数

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

解决方法:将父类中的析构函数改为虚析构函数或纯虚析构函数

虚析构或纯虚析构共性

  • 1、可以解决父类指针释放子类对象
  • 2、都需要有具体的函数实现

区别

  • 如果是纯虚析构,则该类属于抽象类,无法实例化

语法:

//1、虚析构函数
virtual ~类名()
{
	//函数体
}

//2、纯虚析构函数
virtual ~类名() = 0; //
//在类外对纯虚析构函数进行实现
类名::~类名()
{

}
#include<iostream>
using namespace std;

class Animal07
{
public:
	Animal07() {
		cout << "Animal的构造函数调用" << endl;
	}
	//1、虚析构函数
	/*virtual ~Animal07()
	{
		cout << "Animal的虚析构函数调用" << endl;
		if (m_name != NULL) {
			delete m_name;
			m_name = NULL;
		}
	}*/
	//2、纯虚析构函数
	virtual ~Animal07() = 0;

	virtual void speak() = 0;

	string* m_name;
};

//类外实现Animal的纯虚析构函数实现
Animal07::~Animal07() {
	cout << "Animal的纯虚析构函数调用" << endl;
	if (Animal07::m_name != NULL) {
		delete Animal07::m_name;
		Animal07::m_name = NULL;
	}
}
class Cat07 :public Animal07
{
public:
	Cat07(string name) {
		m_name = new string(name);
		cout << "Cat类的构造函数调用" << endl;
	}
	~Cat07()
	{
		cout << "Cat类的析构函数调用" << endl;
		if (m_name != NULL) {
			delete m_name;
			m_name = NULL;
		}
	}

	void speak()
	{
		cout << *m_name << "在说话" << endl;
	}
	string* m_name;
};


void test07_01()
{
	/*下面这段程序打印:
		Animal的构造函数调用
		Cat类的构造函数调用
		Tom在说话
		Animal的析构函数调用
	存在的问题:不能释放子类,造成内存泄漏。解决方法:将父类的析构函数改为虚析构函数
	*/
	Animal07* cat = new Cat07("Tom");
	cat->speak();

	//释放内存
	delete cat;

}

int main()
{
	test07_01();
	system("pause");
	return 0;
}

总结:

  • 1、虚析构和纯虚析构函数都是为了解决通过父类指针释放子类对象
  • 2、如果子类中没有堆区数据,可以不写虚析构和纯虚析构函数
  • 3、拥有纯虚析构函数的类也属于抽象类,不能被实例化

五、文件操作

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

文件类型分为两类:

  • 1、文本文件:文件以ASCII码形式存储在计算机中
  • 2、二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能读懂

操作文件的三大类:

  • 1、ofstream:写操作
  • 2、ifstream:读操作
  • 3、fstream:读写操作

5.1 文本文件

5.1.1 写文件

写文件的步骤:

  • 1、包含头文件:#include<fstream>
  • 2、创建流对象:ofstream ofs;
  • 3、打开文件:ofs.open(“文件路径”,打开方式);
  • 4、写数据:ofs<<“写入的数据”;
  • 5、关闭文件:ofs.close();

文件打开方式:

打开方式解释
ios::in为读文件而打开文件
ios::out为写文件而打开文件
ios::ate初始位置:文件尾
ios::app追加方式写文件
ios::trunc如果文件存在先删除,再创建
ios::binary二进制方式

注意:文件打开方式可以配合使用:利用 “ | ” 操作符

以二进制的方式写文件:ios::binary | ios::out

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

void test01_01() {	
	//文件操作5步:
	//1、包含头文件 #include<fstream>
		
	//2、创建流对象。写的流对象是ofstream
	ofstream ofs;
	//ifstream ifs; //读文件流

	//3、指定打开文件的方式 ofs.open("文件路径",打开方式);
	ofs.open("test01.txt",ios::out);

	//4、写数据
	ofs << "姓名:李柏松" << endl;
	ofs << "性别:男" << endl;
	ofs << "生日:1995-10-12" << endl;

	//5、关闭流
	ofs.close();
}

int main()
{
	test01_01();
	system("pause");
	return 0;
}
5.1.2 读文件

读文件步骤如下:

  • 1、包含头文件 #include<fstream>
  • 2、创建流对象 ifstream ifs;
  • 3、打开文件并判断是否打开成功 ifs.open(“文件路径”, 打开方式);
  • 4、读数据: 四种方式读取
  • 5、关闭文件:ifs.close()
#include<iostream>
using namespace std;
#include<fstream>
#include<string>

void test02_01()
{
	//读文件5步
	//1、包含头文件

	//2、创建读取流对象
	ifstream ifs;

	//3、打开文件并判断是否成功
	ifs.open("test01.txt",ios::in);
	if (!ifs.is_open()) { //判断是否打开成功
		cout << "文件打开失败" << endl;
		return;
	}

	//4、读数据
	//第一种方式
	/*char buf[1024] = { 0 };
	while (ifs >> buf) {
		cout << buf << endl;
	}*/

	//第二种读取方式
	/*char buf[1024] = { 0 };
	while (ifs.getline(buf,sizeof(buf))) {
		cout << buf << endl;
	}*/

	//第三种方式:
	/*string buf;
	while (getline(ifs, buf)) {
		cout << buf << endl;
	}*/

	//第四种方式
	char c;
	while ((c = ifs.get()) != EOF) {
		cout << c;
	}

	//5、关闭文件
	ifs.close();
	
}

int main()
{
	test02_01();

	system("pause");
	return 0;
}

5.2 二进制文件

以二进制的方式对文件进行读写操作,打开方式要指定为 ios::binary

5.2.1 写文件

利用二进制方式写文件要利用流对象进行 write 操作

函数原型:ofs.write((const char*)&buf, int len);

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

//二进制方式可以操作自定义数据类型

class Person
{
public:
	char name[64];
	int age;
};

void test03_01()
{
	//1、包含头文件

	//2、创建流对象
	ofstream ofs;

	//3、打开文件
	ofs.open("person.txt", ios::out | ios::binary);

	//4、写数据
	Person p = { "张三", 19 };
	ofs.write((const char*)&p, sizeof(Person));

	//5、关闭文件
	ofs.close();
}
5.2.2 读文件]

利用流对象read函数

函数原型:ifs.read( char* buffer, int len);

buffer: 指向内存中的一段存储空间,len是读写的字符数

//3、打开文件
	ifs.open("person.txt", ios::in | ios::binary);
	//判断文件是否打开成功
	if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
		return;
	}
	Person p; //创建接受文件数据的对象
	ifs.read((char *)&p, sizeof(Person)); //读取文件
	cout << "姓名:" << p.name << "  年龄:" << p.age << endl;

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

class Person
{
public:
	char name[64];
	int age;
};

void test04_01()
{
	//2、创建流对象
	ifstream ifs;

	//3、打开文件
	ifs.open("person.txt", ios::in | ios::binary);
	//判断文件是否打开成功
	if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
		return;
	}
	Person p; //创建接受文件数据的对象
	ifs.read((char *)&p, sizeof(Person)); //读取文件
	cout << "姓名:" << p.name << "  年龄:" << p.age << endl;

	//关闭文件
	ifs.close();

}


int main()
{
	test04_01();
	system("pause");
	return 0;
}

是否打开成功
cout << “文件打开失败” << endl;
return;
}

//4、读数据
//第一种方式
/*char buf[1024] = { 0 };
while (ifs >> buf) {
	cout << buf << endl;
}*/

//第二种读取方式
/*char buf[1024] = { 0 };
while (ifs.getline(buf,sizeof(buf))) {
	cout << buf << endl;
}*/

//第三种方式:
/*string buf;
while (getline(ifs, buf)) {
	cout << buf << endl;
}*/

//第四种方式
char c;
while ((c = ifs.get()) != EOF) {
	cout << c;
}

//5、关闭文件
ifs.close();

}

int main()
{
test02_01();

system("pause");
return 0;

}


### 5.2 二进制文件

以二进制的方式对文件进行读写操作,打开方式要指定为 ios::binary

#### 5.2.1 写文件

利用二进制方式写文件要利用流对象进行 write 操作

==函数原型:ofs.write((const char*)&buf, int len);==

```c++
#include<iostream>
using namespace std;
#include<fstream>

//二进制方式可以操作自定义数据类型

class Person
{
public:
	char name[64];
	int age;
};

void test03_01()
{
	//1、包含头文件

	//2、创建流对象
	ofstream ofs;

	//3、打开文件
	ofs.open("person.txt", ios::out | ios::binary);

	//4、写数据
	Person p = { "张三", 19 };
	ofs.write((const char*)&p, sizeof(Person));

	//5、关闭文件
	ofs.close();
}
5.2.2 读文件]

利用流对象read函数

函数原型:ifs.read( char* buffer, int len);

buffer: 指向内存中的一段存储空间,len是读写的字符数

//3、打开文件
	ifs.open("person.txt", ios::in | ios::binary);
	//判断文件是否打开成功
	if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
		return;
	}
	Person p; //创建接受文件数据的对象
	ifs.read((char *)&p, sizeof(Person)); //读取文件
	cout << "姓名:" << p.name << "  年龄:" << p.age << endl;

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

class Person
{
public:
	char name[64];
	int age;
};

void test04_01()
{
	//2、创建流对象
	ifstream ifs;

	//3、打开文件
	ifs.open("person.txt", ios::in | ios::binary);
	//判断文件是否打开成功
	if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
		return;
	}
	Person p; //创建接受文件数据的对象
	ifs.read((char *)&p, sizeof(Person)); //读取文件
	cout << "姓名:" << p.name << "  年龄:" << p.age << endl;

	//关闭文件
	ifs.close();

}


int main()
{
	test04_01();
	system("pause");
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值