C++核心编程

C++核心编程

本阶段主要针对C++==面向对象==编程技术做详细讲解,探讨C++中的核心和精髓。

命名空间和c的差异

C++初识

  • 引入头文件 #include <iostream> 标准输入输出流
  • 使用标准命名空间 using namespace std;
  • 标准输出流对象 cout << ; 换行,并刷新缓冲区 endl
  • 将 c 中的 xxx.h头文件 替换 为cxxx。例如 math.h ===》 cmath
  • 面向对象三大特性:封装,继承,多态

<iostream>和<iostream.h>

  • 当使用<iostream.h>时,相当于在c中调用库函数,使用的是全局命名空间
  • 当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用std这块命名空间中的所有标识符,如:cout cin endl

双冒号作用域运算符

::代表作用域。如果前面什么都不加 代表全局作用域

#define _CRT_SECURE_NO_WARNINGS  //处理C4996错误
#include<iostream>

//using namespace std;
//using std::cout; using std::endl;可以在下面直接使用 cout endl

int ack = 1000;
int main()
{
	int ack = 12; 
	std::cout << "ack = " << ack << std::endl;

    // ::ack 表示 访问全局变量 ack
	std::cout << "全局 ack =" << ::ack << std::endl;

	return 0;
}

namespace命名空间

解决名称冲突

案例:

//game1.h
#include <iostream>
using namespace std;

void goAtk();

//game1.cpp
#include "game1.h"

void goAtk(){
    cout << "王者荣耀" << endl;
}

//game2.h
#include <iostream>
using namespace std;

void goAtk();

//game2.cpp
#include "game2.h"

void goAtk(){
    cout << "LOL" << endl;
}

//main
#include "game1.h"
#include "game2.h"
int main(){
    goAtk(); //报错:重定义
}

通过命名空间解决上述问题

//game1.h
#include <iostream>
using namespace std;

namespace King{
	void goAtk();    
}


//game1.cpp
#include "game1.h"

void King::goAtk(){
    cout << "王者荣耀" << endl;
}

//game2.h
#include <iostream>
using namespace std;

namespace LOL{
    void goAtk();
}

//game2.cpp
#include "game2.h"

void LOL::goAtk(){
    cout << "LOL" << endl;
}

//main
#include "game1.h"
#include "game2.h"
int main(){
    King::goAtk();
    LOL::goAtk();
}
//可以存储 变量,结构体 ,类
namespace A{
    int a = 10;
    void fun(){};
    struct Person{};
    class Animal{};
}

//必须声明在全局作用域下
void test(){
	namespace B{ //报错
	
	};
}

//可以嵌套使用
namespace B{
    int m_A = 10;
    namespace C{
        int m_A = 20;
    }
}

void test(){
    cout << "B命名空间下的 m_A" << B:: m_A << endl;
    cout << "B命名空间下的 m_A" << B::C::m_A << endl;
}

// 命名空间是开放的,可以随时向空间中添加新成员
namespace B{
    int m_B = 30;
}
void test02(){
    cout << "B空间下的m_A = " << B::m_A << endl;
    cout << "B空间下的m_B = " << B::m_B << endl;
}

// 命名空间可以是匿名的
namespace{
    int m_C = 100;
    int m_D = 200;
}
void test03(){
    cout << "m_C" << m_C << endl;
    cout << "m_D" << ::m_D << endl;
}

//命名空间可以起别名
namespace veryLongName{
    int m_E = 100;
}
void test04(){
    namespace a = veryLongName;
    cout << veryLongName::m_E << endl;
    cout << a::m_E << endl;
}
  • C中的命名空间

​ 在C语言中只有一个全局作用域

​ C语言中所有的全局标识符共享同一个作用域

​ 标识符之间可能发生冲突

  • C++中的命名空间

​ 命名空间将全局作用域分成不同的部分

​ 不同命名空间中的标识符可以同名而不会发生冲突

​ 命名空间可以相互嵌套

​ 全局作用域也叫默认命名空间

using声明和using编译指令

用的不多,了解即可

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>

using namespace std;

namespace A {
	int a = 1;
}

//using 声明
void test01() {

	//int a = 2;

	//1.using声明
	//using 声明和 就近原则不要同时出现,尽量避免这种情况
	using A::a;
	cout << a << endl;
}

namespace B {
	int a = 2;
}

//using编译指令
void test02() {
	//int a = 3;
	
	//2. using 编译指令
	//using 编译指令和 就近原则同时出现,优先使用就近原则
	//当使用多个编译指令,并且使用同名局部变量时,使用数据依然要加作用域
	using namespace A;
	using namespace B;
    cout << a << enl;
	cout << A::a << endl;
	cout << B::a << endl;
}

int main()
{
	return 0;
}

头文件不应该包含using声明,因为会被拷贝到引用该头文件的文件中。特别是用同事封装的头文件,因为你不知道它定义的变量名,所以引用它的时候会跟自己的变量命名发生冲突

1 内存分区模型

详细见 对c语言补充一章中 内存管理相关内容

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

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

内存四区意义:

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

1.1 程序运行前

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

代码区(text):

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

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

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

全局区(data):

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

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

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

示例:

//全局变量
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"); // 防止黑窗口s'na't

	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关键字进行开辟内存

1.3 new操作符

​ C++中利用new操作符在堆区开辟数据生成指针

​ 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete

​ 语法: new 数据类型

​ 利用new创建的数据,会返回该数据对应的类型的指针

C++在new时的初始化的规律可能为:

对于有构造函数的类,不论有没有括号,都用构造函数进行初始化;

如果没有构造函数,则不加括号的new只分配内存空间,不进行内存的初始化,而加了括号的new会在分配内存的同时初始化为0。----在后面涉及到

示例1: 基本语法

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

int main() {

	int *p = func();

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

	//利用delete释放堆区数据
	delete p;

	//cout << *p << endl; //报错,释放的空间不可访问

	system("pause");

	return 0;
}

示例2:开辟数组

在堆区创建对象数组时,不能为其指定初始值,故一定要提供默认构造函数

//堆区开辟数组
int main() {

	int* arr = new int[10];
    //后期会学到的堆区开辟对象数组, 它一定会调用 默认构造函数。故一定要提供默认构造
    //Person *p = new Person[10];
    //在堆区开辟对象数组,可以没有 默认构造函数
    //Person pArr[10] = {Person(10), Person(20)}; 

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

	for (int i = 0; i < 10; i++)
	{
		cout << arr[i] << endl;
	}
	//释放数组 delete 后加 []
	delete[] arr;

	system("pause");

	return 0;
}

1.4 关于new操作符的一点补充

1. new 的基本用法
  1. C++通过 new 进行动态内存分配

  2. new 在 堆(heap)中开辟空间,而我们指向new 的 变量 存储在 栈中(stack)

  3. new 分配的空间用 delete 释放,new[] 使用 delete new[]

    int* p = new int(5); //表示动态生成一个int,初始化为 5
    int* p = new int[5]; // 表示动态生成一个数组,数组大小为5
    
2. new/delete 和 malloc/free 的区别
  1. malloc和free的函数原型:
void * malloc(size_t size);
void * free(void * pointer);
  1. malloc的使用:
int *p = (int *)malloc(4 * sizeof(int))

​ 一般在使用之前需要验证:

if(p == NULL)
{
	printf("内存分配错误!\n");
}

区别:

  1. 属性

    new,delete是c++中的运算符,需要编译器支持,

    malloc和free是标准库函数,需要加入头文件 stdlib.h

  2. 参数

    malloc需要自行指定动态分配内存的大小,

    而 new 在指定指针类型后可以自动分配内存,无需指定大小

  3. 返回值类型

    malloc 返回的指针需要进行强制类型转换,

    new 操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换

  4. 分配失败

    malloc 内存分配失败返回 NULL

    new 内存分配失败抛出 bac_alloc异常

  5. 自定义类型(指针)

    ​ 一般在创建对象时,需要调用构造函数。对象消亡时,自动调用析构函数。

    ​ new 在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对象的初始化。同理delete也可以自动调用析构函数

    ​ 而malloc只为变量分配内存空间,free只释放了变量的内存

  6. new/delete 比 mallc/free 的效率稍微低一点,因为new/delete的底层封装了 malloc/free

3. delete和 delete[]的区别

delete 只会调用一次析构函数,而 delete[] 会调用每一个成员的析构函数

补充:

  • 不要用 void * 接收 new 出来的对象,利用 void * 无法调用析构函数

    void *  p = new Person;
    delete p;
    
  • 利用 new 创建对象数组

    Person *p = new Person[10];
    //释放时要加 []
    delete[] p;
    
  • **堆区创建对象数组,一定会调用 默认构造函数。**故必须提供默认构造函数

  • 栈上开辟数组,可以没有默认构造函数

2 引用(&变量名)

2.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;

	system("pause");

	return 0;
}

2.2 引用注意事项

  • 引用必须初始化

  • 一个变量可取多个别名

  • 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量

  • 引用不是对象,它在内存空间中没有地址,不能定义引用的引用(指针是对象,有地址)

  • 不要返回局部变量的引用(2.4)

  • 引用只能绑定在对象上,不能绑定字面值或某个表达式的计算结果

  • 如果函数的返回值是引用,这个函数调用可以作为左值进行运算(2.4)

左值:表示可以被取地址的对象。例如:变量、数组元素或结构体成员等。在 C++ 中,左值是可以被修改的,因为它们是存储在内存地址中的标识符,可以通过指针对其进行操作。

右值:表示值本身,而不是可以被取地址的对象。例如:常量、字面量和表达式等。在 C++ 中,右值是不能被修改的,因为它们只是表达式中的值,不能访问其地址。

示例:

int main() {

	int a = 10;
	int b = 20;
	//int &c; //错误,引用必须初始化
    //int &c = 10; //错误,引用必须引一块合法内存	 引用只能绑定在对象上
	int &c = a; //一旦初始化后,就不可以更改
	c = b; //这是赋值操作,不是更改引用

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;

	system("pause");

	return 0;
}

引用和数组

数组引用(数组的别名)的定义:

  • int(&pArr)[10] = arr;

  • typedef int(Arr)[10];

    Arr &pArr = arr;

void test(){
    int arr[10 ] = {0};
    //第一种定义方式
    int(&pArr)[10] = arr;
    for(int i = 0; i < 10; i++){
        a[i] = i;
    }
    for(int i = 0; i < 10; i++){
        cout << pArr[i] << endl;
    }
 
    //第二种定义方式
    typedef int(ARR)[10];
    ARR &pArr2 = arr;
   	for(int i= 0; i < 10; i++){
        cout << pArr2[i] << endl;
    }
}

2.3 引用做函数参数

**作用:**函数传参时,可以利用引用的技术让形参修饰实参

**优点:**可以简化指针修改实参

示例:

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

	system("pause");

	return 0;
}

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

2.4 引用做函数返回值

作用:引用是可以作为函数的返回值存在的

注意:不要返回局部变量引用

用法:函数调用作为左值

示例:

//返回局部变量引用(栈区)
int& test01() {
	int a = 10; //局部变量,函数调用完成后,栈空间被释放
	return a;
}

//返回静态变量引用(全局区)
int& test02() {
	static int a = 20;
	return a;
}

int main() {

	//不能返回局部变量的引用
	int& ref = test01();
	cout << "ref = " << ref << endl; //可以正常输出,因为编译器做了保留
	cout << "ref = " << ref << endl; //乱码,因为a的内存已经释放掉了

	//如果函数做左值,那么必须返回引用
	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	test02() = 1000; //如果函数的返回值是引用,这个函数调用可以作为左值,相当于static int a = 1000

	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	system("pause");

	return 0;
}

2.5 引用的本质

本质:**引用的本质在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++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

2.6 常量引用(常引用)

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

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

示例:

//const引用使用的场景,通常用来修饰形参,这样该形参就不能通过该函数修改
void showValue(const int& v) {
	//v += 10;
	cout << v << endl;
}

int main() {

	//int& ref = 10;  引用本身需要一个合法的内存空间,10在常量区,不在堆或栈中,因此这行错误
	//加入const就可以了,编译器对代码进行优化(增加了一个临时变量):			
    // const int& ref = 10;	====>  int temp = 10; const int& ref = temp;
	const int& ref = 10;

	//ref = 100;  //加入const后变为只读,不可以直接修改变量
    int *p = (int *)&ref;
    *p = 1000;
	cout << ref << endl;
     

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

	system("pause");

	return 0;
}

引用的类型必须与其引用对象的类型一致,但有两个例外:

1)初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转成引用的类型

const int ci = 1024;

const int &r1 = ci; //正确:允许将const int * 绑定到一个普通的 int对象上
//常量引用可以绑定字面值,非常量引用不可以绑定字面值
const int &r3 = 34; //正确,r3是一个常量引用
const int &r4 = r1 * 2; //正确,r4是一个常量引用
    
int &r2 = ci;//错误:ci是常量,r2是一个普通的非常量引用

2)c++Primer P534

2.7 值传递和引用传递

值传递:把实参复制一份传给函数的形参。如果在函数里面修改传给函数的参数值,实际上我们修改的是实参的副本,而在调用函数的位置,这个实参值本身没有改变

地址传递:直接将实参的内存地址(指针)传给形参

引用传递:形参是实参的别名,修改形参相当于修改实参

#include <iostream>
using namespace std;

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

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

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

int main(){
	int a = 10, b = 20; 
//	swap01(a, b);	
//	swap02(&a, &b);
	swap03(a, b);
	
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	 
	
	return 0;
} 

案例一:vector作为函数参数如何传参(其他容器作为函数传参的规则应该和vector一样)

  • 值传递:void 函数名(vector obj); 形参obj改变不会影响实参

  • 引用传递:void 函数名(vector &obj); 形参改变会影响实参

  • 指针传递:void 函数名(vector *pobj); 形参改变会影响实参

数据结构中:链表的操作

  • 当函数参数为LinkList L时,意味着只改变L的内容,而不需要改变L这个指针(LinkList L === Node* L)
  • 当参数为LinkList &L时,意味着需要改变L这个指针本身
  • 当参数为LinkList *L时,意味着需要改变L这个指针指向的LinkList类型的指针

指针和引用作为函数的参数时(作为形参时),形参可以改变实参

2.8总结

引用虽方便,使用须谨慎:
(1)&在这里不是求地址运算,而是起标识作用。

(2)类型标识符是指目标变量的类型。

**(3)**声明引用时,必须同时对其进行初始化。

**(4)**引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。

**(5)**对引用求地址,就是对目标变量求地址。即引用名是目标变量名的一个别名。引用在定义上是说引用不占据任何内存空间,但是编译器在一般将其实现为const指针,即指向位置不可变的指针,所以引用实际上与一般指针同样占用内存。

(6)不能建立引用的数组。因为数组是一个由若干个元素所组成的集合,所以无法建立一个由引用组成的集合,但是可以建立数组的引用。

(7)引用常见的使用用途:作为函数的参数、函数的返回值。

引用和指针的区别和联系:

  1. 指针是一个实体,而引用仅是个别名

  2. 引用使用时无需解引用(*),指针需要解引用

  3. 引用只能在定义时被初始化一次,之后不可变,本质是指针常量,指向不可变;指针可变

  4. 引用不能为空,指针可以为空

  5. “sizeof 引用”得到的是所指向的变量(对象)的大小,

    而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小

  6. 指针和引用的自增(++))自减(–)运算意义不一样

  7. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域

  8. 不要返回一个临时变量的引用

  9. 如果返回对象出了当前函数的作用域依旧存在,则最好使用引用返回,因为这样更高效

  10. 相对而言,引用比指针更安全。使用指针时一定要检查指针是否为空(NULL),且空间回收后指针最好置

    零,以免野指针的发生造成内存泄漏等问题

  11. 存在指针的引用,但不存在指向引用的指针,因为引用不是对象,没有实际地址

两者都是地址的概念,指针指向一块儿内存,其内容为所指内存的地址;引用是某块儿内存的别名。

3 函数提高

内联函数:见c++基础篇函数一章

3.1 函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法: 返回值类型 函数名 (参数= 默认值){}

示例:

  • 如果函数中某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
  • 如果函数的声明有默认参数,函数的实现就不能有默认参数。不能同时加默认参数
// 如果我们自己传入数据,就用自己的数据,如果没有,那么用默认值
int func(int a, int b = 10, int c = 10) {
	return a + b + c;
}

//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//int func2(int a = 10; int b, int c, int d) //报错
//{
//    return a + b + c;
//}

//2. 如果函数的声明有默认参数,函数的实现就不能有默认参数
// 声明和实现只能有一个有默认参数
int func2(int a = 10, int b = 10); 

int func2(int a, int b) { //函数的声明有默认参数了,所以在这里不能有默认参数
	return a + b;
}

int main() {
	cout << "ret = " << func(20, 20) << endl;
	cout << "ret = " << func(100) << endl;

	system("pause");

	return 0;
}

3.2 函数占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型){}

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术(运算符重载)

示例:

//函数占位参数:只写一个类型进行占位,调用的时候必须要传入占位值
//占位参数也可以有默认参数,比如下面的 int a 可以 写成 int a = 10,int 可以写成 int = 10
void func(int a, int) {
	cout << "this is func" << endl;
}

int main() {

	func(10,10); //占位参数必须填补。但如果采用func(int a, int = 10) 这种方式,占位参数也可以不用填补

	system("pause");

	return 0;
}

3.3 函数重载

3.3.1 函数重载概述

编译器会将重载的函数的函数名进行修饰,用来区分重载的不同的函数

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

函数重载满足条件:

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

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

示例:

//1. 函数重载需要函数都在同一个作用域下
void func()
{
	cout << "func 的调用!" << endl;
}
void func(int a)
{
	cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
	cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
	cout << "func (int a ,double b) 的调用!" << endl;
}

void func(double a ,int b)
{
	cout << "func (double a ,int b)的调用!" << endl;
}

//2. 函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
//	cout << "func (double a ,int b)的调用!" << endl;
//}


int main() {

	func();
	func(10);
	func(3.14);
	func(10,3.14);
	func(3.14 , 10);
	
	system("pause");

	return 0;
}

补充:c语言中没有函数重载的概念,但是有可变参数的概念。c通过可变参数实现c++中的重载功能

如 int printf(const char *format, …); 形参列表中的 … 即为可变参数,可以指定多个形参。

3.3.2 函数重载注意事项

避免二义性:

  • 引用作为重载条件(加const 和不加const)
  • 函数重载碰到函数默认参数

示例:

//函数重载注意事项
//1、引用作为重载条件,不要同时出现 void func(int a){}; 调用时会出现二义性

void func(int &a) //int &a 读取变量
{
	cout << "func (int &a) 调用 " << endl;
}

void func(const int &a) // const int &a 读取常量
{
	cout << "func (const int &a) 调用 " << endl;
}


//2、函数重载碰到函数默认参数,注意避免二义性
void func2(int a, int b = 10)
{
	cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
	cout << "func2(int a) 调用" << endl;
}

int main() {
	
	int a = 10;
	func(a); //调用无const
	func(10);//调用有const


	//func2(10); //碰到默认参数产生歧义,需要避免,如果int b = 10 改为 int b 就不会报错

	system("pause");

	return 0;
}

后面还会学到运算符重载也会构成函数重载

3.3.3 函数重载的实现原理

C++利用 name mangling(倾轧(yà))技术,来改名函数名,区分参数不同的同名函数。

实现原理:用 v c i f l d 表示 void (char int float long double) 及其引用。

void func(char	a);	//	func_c(char	a)	
void func(char	a,	int	b,	double	c);	//func_cid(char	a,	int	b,	double	c)

3.4 extern "c"浅析

用途:在c++中调用c语言文件

c++中有函数重载,会对函数名称做修饰,导致c语言的函数链接失败,利用extern "C"可以解决

方法1:

  • 在c++文件中加入:extern ”C“ void show();告诉编译器,用c语言的方式链接show()函数。

    不推荐,调用 多个c语言函数就要写多次上述语句

方法2:

  • 在c语言的头文件中加入 6 行代码

    #ifdef __cplusplus //两个下划线
    extern "C" {   
    #endif
    //头文件中内容
    #ifdef __cplusplus
    }
    #endif
    

案例:

//test.h 使用这种方法要包含头文件 #include "test.h"
#ifdef __cplusplus //两个下划线
extern "C" {
#endif
#include<stdio.h>
void show();
#ifdef __cplusplus
}
#endif

//test.c
#include "test.h"

void show() {
	printf("hello world\n");
}

//main.cpp
#include "test.h"
using namespace std;

//告诉编译器,show函数用c语言的方式做链接
//extern "C" void show(); //使用这种方法要注销掉#include "test.h"

int main() {
	//C++中会修饰函数名,但show是c语言文件,因此链接失败
	show(); //报错:一个无法解析的外部命令(链接阶段出错)
	return 0;
}

4 类和对象

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

C++认为万事万物都皆为对象,对象上有其属性和行为

例如:

​ 人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…

​ 车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…

​ 具有相同性质的对象,我们可以抽象称为,人属于人类,车属于车类

类的分文件编写

防止头文件重复包含:见基础篇,函数一章 6.7

//cir.h文件
#ifndef __CIR_H__(也可以写为 CIR_H)
#define __CIR_H__
#include <iostream>
using namespace std;
class Cir{
    //属性,方法声明
    public:
    	int x, y;
     	int add(int x, int y);
}
#endif

// cir.cpp文件
#include "cir.h"
//方法的实现,要在方法名前加作用域
//如果不加,则是全局函数。加了作用域之后,是该类下的成员函数
int Cir::add(int x, int y){
   return x + y; 
}

4.1 封装

4.1.1 封装的意义(访问权限)

c语言中的属性和行为是分离的

封装是C++面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

封装意义一:

​ 在设计类的时候,属性和行为写在一起,表现事物

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

**示例1:**设计一个圆类,求圆的周长

示例代码:

//圆周率
const double PI = 3.14;

//1、封装的意义
//将属性和行为作为一个整体,用来表现生活中的事物

//封装一个圆类,求圆的周长
//class代表设计一个类,后面跟着的是类名
class Circle
{
public:  //访问权限  公共的权限

	//属性, 默认访问权限为 private
	int m_r;//半径

	//行为
	//获取到圆的周长
	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;
}

**示例2:**设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号

示例2代码:

//学生类
class Student {
public:
	void setName(string name) {
		m_name = name;
	}
	void setID(int id) {
		m_id = id;
	}

	void showStudent() {
		cout << "name:" << m_name << " ID:" << m_id << endl;
	}
public:
	string m_name;
	int m_id;
};

int main() {

	Student stu;
	stu.setName("德玛西亚");
	stu.setID(250);
	stu.showStudent();

	system("pause");

	return 0;
}

封装意义二:

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

访问权限有三种:

  1. public 公共权限 类内可以访问 类外可以访问
  2. protected 保护权限 类内可以访问 类外不可以访问
  3. 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; //私有权限类外访问不到

	system("pause");

	return 0;
}
4.1.2 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; //正确,访问权限是公共

	system("pause");

	return 0;
}

访问权限

  • public 公共权限 成员 类内,类外都可以访问

  • private 私有权限 成员 类内可以访问,类外不可以访问

  • protected 保护权限 成员 类内可以访问,类外不可以访问

    class A{
    public:
        int a;
    protected:
        int b;
    private:
        int c;
        
    public:
        void fun(){
            a = 10;
            b = 20;
            c = 30;
        }
    }
    
    void test(){
        Person p;
        cout << p.a; //ok
        p.b; // no
        p.c; // no
    }
    
4.1.3 成员属性设置为私有

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

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

示例:

class Person {
public:
	//姓名设置可读可写
	void setName(string name) {
		m_Name = name;
	}
	string getName()
	{
		return m_Name;
	}

	//获取年龄 
	int getAge() {
		return m_Age;
	}
    
	//设置年龄
	void setAge(int age) {
		if (age < 0 || age > 150) {
			cout << "你个老妖精!" << endl;
			return;
		}
		m_Age = age;
	}

	//情人设置为只写
	void setLover(string lover) {
		m_Lover = lover;
	}

private:
	string m_Name; //可读可写  姓名
	
	int m_Age; //只读  年龄

	string m_Lover; //只写  情人
};


int main() {

	Person p;
	//姓名设置
	p.setName("张三"); //"张三" 是 char * 类型,这里做了一个隐式类型转换:char * --> string
	cout << "姓名: " << p.getName() << endl;

	//年龄设置
	p.setAge(50);
	cout << "年龄: " << p.getAge() << endl;

	//情人设置
	p.setLover("苍井");
	//cout << "情人: " << p.m_Lover << endl;  //只写属性,不可以读取

	system("pause");

	return 0;
}
4.1.4 c++中创建对象的三种方式

1)A a;A a = A() ; 在栈(stack)上分配空间; 使用完后不需要手动释放,该类析构函数会自动执行
2)A *a;只是声明,还没有分配空间;
3)A *a= new A;在堆(heap)上分配空间;只有调用到delete时才会执行析构函数,如果程序退出而没有执行delete则会造成内存泄漏。

class  Test {   
  private:  
  public:  
      add()
      {
         int x,y,sum;
         x=5;
         y=5;
         sum=x+y;
         cout<<sum<<endl;
     }
 };  
 void main()  
 {  
    Test test1;              //栈中分配  ,由操作系统进行内存的分配和管理
    Test test2 = Test();       //栈中分配  ,由操作系统进行内存的分配和管理
     
    Test *test3 = new Test();  //堆中分配  ,由管理者进行内存的分配和管理,用完必须delete(),否则可能造成内存泄漏  Test *test3 = new Test;
    test1.add();
    test2.add();             //"." 是结构体成员引用
    test3->add();            //"->"是指针引用
     
    delete(test3);
    system("pause"); 
}
4.1.5 匿名对象

在 C++ 中,可以通过直接使用类名后接括号(包含构造函数所需的参数,如果有的话)的方式定义一个匿名对象。匿名对象是没有名称的临时对象,其生命周期在创建时开始,在表达式结束时销毁。

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

        ~Sample() {
            cout << "析构函数被调用" << endl;
        }
      	
    	void display() {
            cout << "显示信息" << endl;
        }
};

int main() {
    // 创建一个匿名对象
    Sample();
    
    //再次创建匿名对象,调用display()函数
    Sample().display();

    system("pause");
    return 0;
}

练习案例1:设计立方体类

设计立方体类(Cube)

求出立方体的面积和体积

分别用全局函数和成员函数判断两个立方体是否相等。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

练习案例2:点和圆的关系

设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2 对象的初始化和清理

  • 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
  • C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
4.2.1 构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题

​ 一个对象或者变量没有初始状态,对其使用后果是未知

​ 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

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

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

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

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

​ 如果程序中未声明,则系统自动产生出一个隐含的参数列表为空的构造函数
​ 允许为内联函数、重载函数、带默认形参值的函数

  • 析构函数:主要作用在于对象销毁前系统自动调用,用于在对象被销毁之前释放其占用的内存空间、清理堆 栈、传递数据等等。

    ​ 在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间。
    ​ 如果程序中未声明析构函数,编译器将自动产生一个隐含的析构函数。

  • 拷贝构造函数:用于将一个对象的值复制到新的对象中。它是一种特殊的构造函数,用于在创建对象时从 同类对象中创建一个新对象,并将其初始化为原始对象的副本。

​ 简单来说,拷贝构造函数的作用是创建新对象,并将其初始化为源对象的副本。

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

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载。可默认参数
  4. 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次
  5. 必须声明在全局作用域下(public)

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

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载,也不能使用默认参数
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
  5. 必须声明在全局作用域下(public)

拷贝构造函数:类名(const 类名& p){}

class Person
{
public:
	//构造函数
	Person()
	{
		cout << "Person的构造函数调用" << endl;
	}
	//析构函数
	~Person()
	{
		cout << "Person的析构函数调用" << endl;
	}
    
    //析构函数的另一种定义方式
    //~Person();  //先声明

};

//Person::~Person(){ // 后定义
//    cout << "Person的析构函数调用" << endl;
//}

void test01()
{
	Person p;
}

int main() {
	
	test01();

	system("pause");

	return 0;
}
4.2.2 构造函数的分类及调用

两种分类方式:

  • ​ 按参数分为: 有参构造和无参构造(即:默认构造函数)

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

三种调用方式:

  • ​ 括号法

  • ​ 显示法

  • ​ 隐式转换法

构造函数调用规则如下:

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

  • 如果用户定义拷贝构造函数,c++不再提供其他构造函数(无参构造,有参构造)

示例:

注意1:调用无参构造函数不能加括号

注意2:不能利用 拷贝构造函数 初始化 匿名对象

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

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int a) {
		age = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数 将 p 的属性拷贝给新的变量,对象......
	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:调用无参构造函数不能加括号
    //如果加了编译器会认为这是一个函数声明( 形式:void fun() ) 返回类型 方法名() ----》 Person p2()
	//Person p2(); 
    Person p2; //默认构造函数调用
    Person p3(p2); //拷贝构造函数

	//2.2 显式法
    Person p1;
	Person p2 = Person(10); 
	Person p3 = Person(p2);
    
	//Person(10)单独写就是匿名对象  当前行执行结束之后,马上调用析构函数
    //匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象,调用析构函数 
    // Person(10);

	// 注意2:不能利用 拷贝构造函数 初始化 匿名对象   
    // 编译器认为是对象声明 即:Person (p3) == person p3
    //Person(p3)
	//Person p5(p4);
    
    //2.3 隐式转换法(注意和 explicit关键字 的联系)
	Person p4 = 10; // 相当于 Person p4 = Person(10); 
	Person p5 = p4; // 相当于 Person p5 = Person(p4); 
    
    //在堆区创建对象
    Person p = new Person; //调用构造函数
    delete p; //调用析构函数
    //注意:不要用 void* 接收 new 出来的对象
    void * p3 = new Person; //调用构造函数
    delete p3; //不会调用析构函数,因为不知道 p3 的数据类型,不知道要释放多大的空间
}

int main() {

	test01();
	//test02();

	system("pause");

	return 0;
}
4.2.3 拷贝构造函数调用时机(4.6.5)

拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。

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

  • 使用一个已经创建完毕的对象来初始化一个新对象(将一个类的对象赋值给该类的另一个对象)

  • 值传递的方式给函数参数传值(函数的形参为类的对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数)

  • 以值方式返回局部对象(当函数的返回值是类对象时,系统自动调用拷贝构造函数)

注意:派生类对象初始化基类对象不会调用拷贝构造函数

示例:

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); //调用拷贝构造函数
	Person newman2 = man; //拷贝构造

	//Person newman3;
	//newman3 = man; //不是调用拷贝构造函数,赋值操作
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
	Person p; //默认无参构造函数
	doWork(p); // 实参p 传给形参 p1 时会调用拷贝构造函数,在dowork里面修改的是p1,不会影响p
}

//3. 以值方式返回局部对象
Person doWork2()
{
	Person p1;
	cout << (int *)&p1 << endl;
	return p1; //这里的p1 不是上面的p1,而是根据上面的p1创建的新的对象
}

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


int main() {

	//test01();
	//test02();
	test03();

	system("pause");

	return 0;
}
4.2.4 构造函数调用规则

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

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

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

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

构造函数调用规则如下:

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

  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数(无参构造,有参构造)

示例:

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

void test01()
{
	Person p1(18);
	//如果用户不提供拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
	Person p2(p1);

	cout << "p2的年龄为: " << p2.age << endl;
}

void test02()
{
	//如果用户提供有参构造,编译器不会提供默认无参构造,但会提供拷贝构造
	Person p1; //此时如果用户自己没有提供默认构造,会出错
	Person p2(10); //用户提供的有参
	Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供

	//如果用户提供拷贝构造,编译器不会提供其他构造函数
	Person p4; //此时如果用户自己没有提供默认构造,会出错
	Person p5(10); //此时如果用户自己没有提供有参,会出错
	Person p6(p5); //用户自己提供拷贝构造
}

int main() {

	//test01();
	test02();
    
	system("pause");

	return 0;
}
4.2.5 深拷贝与浅拷贝

深浅拷贝是面试经典问题,也是常见的一个坑

浅拷贝:简单的赋值拷贝操作(统默认提供的拷贝构造函数 进行的就是 浅拷贝)

深拷贝:在堆区重新申请空间,进行拷贝操作(需要自己实现深拷贝)

浅拷贝

  • 实现对象间数据元素的一一对应复制。(值传递)

深拷贝

  • 当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指的对象进行复制。(地址传递)

示例:

浅拷贝问题一般出现在 类中成员在堆区创建,赋值时指向同一片堆区空间,释放时释放同一片堆区空间多次

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int age ,int height) {
		
		cout << "有参构造函数!" << endl;

		m_age = age;
		m_height = new int(height); //new的数据存放在堆区
		
	}
    
    //浅拷贝 系统默认提供的拷贝构造函数 进行的就是 浅拷贝
    Person(const Person& p){
        cout << "拷贝构造函数!"  << endl;
        m_age = p.m_age;
        m_height = p.m_height;
    }
    
    
	//自己实现拷贝构造函数,解决浅拷贝带来的问题
	Person(const Person& p) {
		cout << "拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        //浅拷贝:将 p.m_height 的值,也就是 其所指向堆区空间的地址,赋值给了新对象,在调用析构函数释放内存的时候,会把 p.m_height 所指向的堆区空间重复释放两次(第一次:新对象释放,第二次:p释放。析构函数调用跟构造函数调用顺序相反)
		m_age = p.m_age;
        // m_height = p.mheight; 编译器默认实现的就是这行代码
		m_height = new int(*p.m_height);  // *p.m_height 即 p.m_height所指向堆区空间的值
		
	}

	//析构函数,将堆区开辟的数据做释放操作
	~Person() {
		cout << "析构函数!" << endl;
		if (m_height != NULL)
		{
			delete m_height;
            m_height = NULL; //防止野指针的出现,对delete后的指针进行置空操作
		}
	}
public:
	int m_age;
	int* m_height;
};

void test01()
{
	Person p1(18, 180);

	Person p2(p1);

	cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;

	cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}

总结:

  • 如果属性有在堆区开辟的(new出来的属性),一定要自己提供拷贝构造函数,防止浅拷贝带来的问题(堆中的内存重复释放)

  • 如果自己没有实现拷贝构造函数,系统将自动创建默认拷贝构造函数,做浅拷贝操作,也就是进行简单的赋值操作。对于普通变量,这种简单的赋值操作并没有什么问题。

  • 而对于指针变量来讲,数据存放在堆区,而该指针变量存放的是该数据在堆区的地址,进行赋值操作后,所拷贝的对象也有一个指针变量存放该数据所在的内存地址。

    那么,在对象销毁前,调用析构函数释放堆中的内存的操作会进行两次,原对象一次,拷贝的对象一次,且都是针对堆中的同一块内存,导致异常发生。

  • 解决方法:

    ​ 自己实现拷贝构造函数,向上例中一样,用new在堆中其他位置重新开辟一片空间,保存该数据 的副本,然后指针变量存放这片新空间的内存。那么在调用析构函数释放堆中的内存的时候,是 释放堆中不同空间的内存,不会出现重复释放堆区的问题

4.2.6 初始化列表

作用:

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

语法:构造函数():属性1(值1),属性2(值2)... {} 直接给属性初始化

构造函数(形参1, 形参2.....):属性1(形参1),属性2(形参2)...{} 将属性初始化为相应的形参值

示例:

class Person {
public:

	传统方式初始化
	//Person(int a, int b, int c) {
	//	m_A = a;
	//	m_B = b;
	//	m_C = c;
	//}

	//初始化列表方式初始化
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
	void PrintPerson() {
		cout << "mA:" << m_A << endl;
		cout << "mB:" << m_B << endl;
		cout << "mC:" << m_C << endl;
	}
private:
	int m_A;
	int m_B;
	int m_C;
};

int main() {

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


	system("pause");

	return 0;
}
4.2.7 类对象作为类成员(组合)

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

例如:

class A {}
class B
{
    A a;
}

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

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

构造的顺序是 :先调用对象成员的构造函数(B),如果有多个对象成员,则按对象声明的顺序,先声明者先调用构造函数;再调用本类的构造函数(A)
析构函数调用顺序与构造函数相反

  • 初始化列表中未出现的对象成员,用默认构造函数(即无形参的)初始化

  • 系统自动生成的隐含的默认构造函数中,内嵌对象全部用默认构造函数初始化

构造函数的顺序是先调用对象成员的构造函数,再调用本类的构造函数,

对象成员的构造函数相当于在栈的底部,上面是本类的构造函数。相当于压栈操作

所以调用析构函数时,先调用本类的析构函数,在调用对象成员的析构函数。相当于弹栈操作

示例:

class Phone
{
public:
	Phone(string name)
	{
		m_PhoneName = name;
		cout << "Phone构造" << endl;
	}

	~Phone()
	{
		cout << "Phone析构" << endl;
	}

	string m_PhoneName;

};

class Game
{
public:
	Game(string name)
	{
		m_GameName = name;
		cout << "Game构造" << endl;
	}

	~Game()
	{
		cout << "Game析构" << endl;
	}

	string m_GameName;

};


class Person
{
public:

	//初始化列表可以告诉编译器调用哪一个构造函数
    //m_Phone(pName)相当于 Phone m_Phone = pName 隐式转换法,所以它要先去调用Phone类(对象成员类)的构造方法,再执行下面Person(本类)的构造方法
	Person(string name, string pName, string gName) :m_Name(name), m_Phone(pName),m_Game(gName)
	{
		cout << "Person构造" << endl;
	}

	~Person()
	{
		cout << "Person析构" << endl;
	}

	void playGame()
	{
		cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! "  << "打 " << m_Game.m_GameName << endl;
	}

	string m_Name; //包含头文件 #include<string>
	Phone m_Phone; //对象声明的顺序 m_Phone m_Game
    Game m_Game;

};
void test01()
{
	//当类中成员是其他类对象时,我们称该成员为 对象成员
	//构造的顺序是 :先调用对象成员的构造,再调用本类构造
	//析构顺序与构造相反
	Person p("张三" , "苹果X", "王者荣耀");
	p.playGame();

}


int main() {

	test01();

	system("pause");

	return 0;
}
4.2.8 explicit 关键字

作用:

  • 禁止通过构造函数进行隐式转换(4.2.2)。声明为 explicit 的 构造函数 不能在 隐式转换中被使用

  • explicit 用于修饰构造函数,防止隐式转换

  • 是针对 单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数)而言

    class MyString{
    public:
        MyString(char * str){
            cout << "char * 被调用" << endl;
        }
        // explicit用途:防止利用隐式类型转换的方式来构造对象
        explicit MyString(int len){
            cout << "int 被调用" << endl;
        }
    };
    
    int main(){
        MyString str1(10); //调用第二个
        MyString str2 = MyString(100); //调用第一个
    
        //隐式转换 发生歧义:10 和 "10"调用的是两个不同的构造函数
        //将第二个函数声明为 explicit, 则 传 10 会报错,即 第二个构造函数不能使用
         MyString str3 = "10";
    
        system("pause");
        return 0;   
    }
    
4.2.9 静态成员(全局区)

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

不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享

静态成员分为:

  • 静态成员必须在类中声明,在类外初始化。
  • 静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间
  • 静态数据成员可以通过类名或对象名来引用
  • 静态成员变量

    • 所有对象共享同一份数据
    • 在编译阶段分配内存,存储在 data 区
    • 类内声明,类外初始化,用(::)来指明所属的类
  • 静态成员函数

    • 所有对象共享同一个函数

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

静态成员变量

静态成员变量特点:

  1. 在编译阶段分配内存
  2. 类内声明,类外初始化 因为它是该类所有对象共享的,所以不能在类内进行初始化
  3. static 成员是命名空间属于类的全局变量,存储在 data 区,求类大小,并不包含在内。
  4. 所有对象共享同一份数据
  5. 静态成员变量也是有访问权限的
  6. 访问方式:通过对象访问,通过类名访问
class Person
{
	
public:
	static int m_A; //静态成员变量

private:
	static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;
int Person::m_B = 10;

void test01()
{
     // 静态成员变量,不属于某个对象,所有对象都共享同一份数据
    // 因此静态成员变量有两种访问方式
    
	//静态成员变量两种访问方式
	//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; //私有权限访问不到
}

int main() {

	test01();

	system("pause");

	return 0;
}
静态成员函数

静态成员函数特点:

  1. 所有对象共享同一个函数

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

    不能访问非静态成员变量,非静态成员函数,在调用时this 指针被当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针

  3. 静态成员函数也是有访问权限的

  4. 访问方式:通过对象访问,通过类名访问

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;


void test01()
{
	//静态成员变量两种访问方式

	//1、通过对象
	Person p1;
	p1.func();

	//2、通过类名
	Person::func();


	//Person::func2(); //私有权限访问不到
}

int main() {

	test01();

	system("pause");

	return 0;
}

静态成员函数与普通成员函数的区别

  • 静态成员函数不包含指向具体对象的this指针

  • 普通成员函数包含一个指向具体对象的this指针

4.2.10 单例设计模式

单例模式的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约资源。

实现方法:

​ 通过一个类,只能实例化唯一的一个对象

​ 私有化默认构造函数,拷贝构造函数(无法通过拷贝构造函数创建多个对象),唯一静态实例指针

​ 对外提供 静态getInstance 接口,将指针返回

1. 静态成员实现单例设计模式

打印主席类

class ChairMan{
public:
    static ChairMan* getInstance(){
        return signalMan;
    }
private:
    //构造函数私有化,不可以创建多个对象
    ChairMan(){};
    //析构函数私有化,不可以用已有对象创建新对象(确保 单例)
    ChairMan(const ChairMan& c){};
private:
    //将主席指针私有化,对外提供访问接口
    static ChairMan * signalMan; //类内声明,类外初始化(声明为static,是为了让该单例被共享)
};

ChairMan * ChairMan::signalMan = new ChairMan();


int main(){
    ChairMan * c1 = ChairMan::getInstance();
    ChairMan * c2 = ChairMan::getInstance();
    if(c1 == c2){
        cout << "c1 == c2" << endl;
    }else{
        cout << "c1!= c2" << endl;
    }
    
    system("pause");
    return 0;
}

案例二:打印机类

class Printer{
public:
    static Printer * getInstance(){
        return printer;
    }
    
    void printText(string test){
        m_Count++;
        cout << test << endl;
    }
    
    int m_Count;

private:
    Printer(){
        m_Count = 0;
        cout << "Printer()被调用" << endl;
    };
    Printer(const Printer& p){};
private:
    static Printer * printer;
};

Printer * Printer::printer = new Printer();

int main(){
    cout << "main()被调用" << endl;

    cout << "=======================" << endl;

    Printer * p1 = Printer::getInstance();
    p1->printText("入职证明");
    p1->printText("离职证明");
    p1->printText("加薪申请");
    p1->printText("旅游证明");

    cout << "打印机使用次数:" << p1->m_Count << endl;

    Printer * p2 = Printer::getInstance();
    p2 -> printText("调休申请");
    
    cout << "打印机使用次数:" << p2->m_Count << endl;

    system("pause");
    return 0;
}

由运行结果可知

  • 该案例实现了单例,因为 p1 和 p2 共享了 变量 m_Count

  • Printer() 先调用,main()后调用。因为静态成员函数在编译时分配内存

单例模式可以实现构造函数先调用,main()函数后调用

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

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

    • 只有非静态成员变量才属于类的对象上,占用对象空间。而静态成员变量,静态成员函数,普通函数都不占用对象空间。

    • 普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式

      静态成员变量:存储于全局数据区(data)中

      成员函数:存储于代码段中。

  • c++编译器会给每个空对象也分配一个字节空间,是为了区分不同空对象占内存的位置,防止不同空对象占用同一片内存

class Person {//类也有内存对齐规则,具体可以看 c基础 8.11
public:
	Person() {
		mA = 0;
	}
	//非静态成员变量占对象空间
	int mA;
	//静态成员变量不占对象空间
	static int mB; 
	//函数也不占对象空间,所有函数共享一个函数实例
	void func() {
		cout << "mA:" << this->mA << endl;
	}
	//静态成员函数也不占对象空间
	static void sfunc() {
        cout << "mb:" << this->mB << endl;
	}
};
int Person::mB = 100;

int main() {

	cout << sizeof(Person) << endl; //4 只有int变量所占的空间被计算了

	system("pause");

	return 0;
}

空对象:

class Person
{

};

int main()
{
	Person p;
	//空对象占用的内存空间为: 1
    //c++编译器会给每个空对象也分配一个字节空间,是为了区分不同空对象占内存的位置,防止不同空对象占用同一片内存
    //Person pArr[10]; //10个空对象,给它们分配空间,防止它们占用同一片内存
	cout << "size of p = " << sizeof(p) << endl;
}
4.3.2 this指针概念

通过4.3.1我们知道在C++中成员变量和成员函数是分开存储的

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

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

c++通过提供特殊的对象指针,this指针,解决上述问题。this指针 指向 被调用的成员函数 所属的对象

即:谁调用成员函数,this指针指向谁

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

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

this指针本质上是一个指针常量:对象数据类型 * const this 指针指向不可以改,指针指向的值可以改

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
class Person
{
public:

	Person(int age)
	{
		//1、当形参和成员变量同名时,可用this指针来区分
		this->age = age;
	}

    //Person& 返回的是这个对象的本身,如果换成Person,返回的是拷贝出来的副本
    //链式编程思想
	Person& PersonAddPerson(Person p)
	{
		this->age += p.age;
		//返回对象本身
        //this是指向p2的指针,则*this指向的就是p2这个对象
		return *this;
	}

	int age;
};

void test01()
{
	Person p1(10);
	cout << "p1.age = " << p1.age << endl;
	
}

void test02()
{
    Person p1(10);
    Person p2(10);
    
	p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
  	//如果把上面函数中的Person& 换成Person,则上面的语句会调用拷贝构造函数,每次返回的其实是p2的不同副本,累加操作在副本上进行,无法影响 p2 本身。以上语句相当于 p2 =  p2.PersonAddPerson(p1); p2 = p2.PersonAddPerson(p1);  p2 =  p2.PersonAddPerson(p1);
	cout << "p2.age = " << p2.age << endl;
}

int main() {

	//test01();
    
    test02();

	system("pause");

	return 0;
}
4.3.3 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针,如果成员函数中用到了this指针,就不可以用空指针调用成员函数

如果用到this指针,需要加以判断保证代码的健壮性

示例:

//空指针访问成员函数
class Person { //this的本质:Person * const this;
public:

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

	void ShowPerson() 
    {
	//	if (this == NULL) { //加上这行代码就不会崩了
	//		return;
	//	}
        //报错原因是:this指向的对象p 为 NULL, 该句相当于 this ->mAge  即 NULL->mAge
		cout << mAge << endl; // 相当于 cout << this -> mAge << endl; 
	}

public:
	int mAge;
};

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

int main() {

	test01();

	system("pause");

	return 0;
}
4.3.4 const修饰成员函数(常函数,常对象)

常函数:

  • 成员函数后加const后我们称为这个函数为常函数。const目的是为了修饰 this指针,让指针指向的值不可以修改

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

    普通函数的this指针:对象数据类型 * const this

    ​ 指向不可以变,但指向的值可以改变

    常函数的this指针: const 对象数据类型 * const this

    ​ 指向不可以变,指向的值也不可以改变,故其不能修改成员属性的值

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

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数
  • 常对象不能修改成员变量的值,但是可以访问
  • 常对象可以修改mutable修饰成员变量

示例:

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

	//this指针的本质是一个指针常量: Person* const this; 指针的指向不可修改
    //this = NULL; //不能修改指针的指向 Person* const this;
	//this->mA = 100; //但是this指针指向的对象的数据是可以修改的,前提是this指针常量前面没有const修饰
    
	//如果想让指针指向的值也不可以修改,需要声明常函数
    //相当于以前的 this 是 Person* const this;将其改为 const Person* const this,则其指向的值也不可以修改
	void ShowPerson() const 
    {
		//const修饰成员函数,表示指针指向的内存空间的数据不能修改,相当于const Person * const this;除了mutable修饰的变量
        //this->mA = 100 //报错
		this->m_B = 100;
	}

	void MyFunc() const //没加const的话,下面的mA = 10000;不会报错
    {
		//mA = 10000;
	}

public:
	int m_A;
	mutable int m_B; //常函数中,有些特殊的属性想修改,在属性前加 mutable 关键字
};


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

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

	//常对象访问成员函数
	person.ShowPerson() ; //常对象只能调用常函数
    //调用 MyFunc() 没加const的话 下面会报错
    //person.MyFunc(); //常对象不可以调用普通成员函数,因为普通成员函数可以修改属性

}

int main() {

	test01();

	system("pause");

	return 0;
}

4.4 友元(friend)

生活中你的家有客厅(Public),有你的卧室(Private)

客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去

但是呢,你也可以允许你的好闺蜜好基友进去。

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

友元的目的就是让一个函数或者类 访问另一个类中私有成员,即可以访问该类中的所有成员,

友元函数不是类的成员函数

友元关系是单向的:如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。

友元的关键字friend

友元的三种实现

  • 全局函数做友元

  • 类做友元

  • 成员函数做友元

友元关系是单向的:

​ 如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函 数却不能访问B类的私有、保护数据。

4.4.1 全局函数做友元

friend 返回值类型 全局函数名(形参列表);

class Building
{
	//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容
	friend void goodGay(Building * building);

public:

	Building()
	{
		this->m_SittingRoom = "客厅";
		this->m_BedRoom = "卧室";
	}


public:
	string m_SittingRoom; //客厅 注意加上预处理命令 #include<string>

private:
	string m_BedRoom; //卧室
};

//类外边的全局函数
//如果想访问Building类的private属性,必须在Building类做出如下声明
// friend void goodGay(Building * building);
void goodGay(Building * building)
{
	cout << "好基友正在访问: " << building->m_SittingRoom << endl;
	cout << "好基友正在访问: " << building->m_BedRoom << endl; //访问Building类的private属性
}


void test01()
{
	Building b;
	goodGay(&b);
}

int main(){

	test01();

	system("pause");
	return 0;
}

4.4.2 类做友元

friend class 类名;

C++中的前向引用包括两个概念:类的前向声明和函数的前向声明。

前向引用是 C++ 中一种通知编译器有一个实体存在,但暂时没有其定义的声明。前向引用既可用于类,也可用于函数,能够减少头文件的依赖和提高编译速度。

class Building; //类的声明(前向引用)
class goodGay
{
public:

	goodGay();
	void visit();  //访问 Building中的属性

private:
	Building *building;
};


class Building
{
	//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
	friend class goodGay;

public:
	Building();

public:
	string m_SittingRoom; //客厅 加 #include <string>
private:
	string m_BedRoom;//卧室
};

// 类外写函数和方法 类名::方法名(参数列表)
Building::Building() // Building类的构造器
{
	this->m_SittingRoom = "客厅";
	this->m_BedRoom = "卧室";
}

goodGay::goodGay() //goodGay类的构造器
{
    // 加括号调用没有参数的构造函数,不加括号调用默认构造函数或唯一的构造函数
	building = new Building; //在堆区创建对象,并用指针指向该对象
}

void goodGay::visit() //goodGay类的方法
{
	cout << "好基友正在访问" << building->m_SittingRoom << endl; //公有属性
	cout << "好基友正在访问" << building->m_BedRoom << endl; // 私有属性,没有定义友元的话无法访问
}

void test01()
{
	goodGay gg; //创建goodGay类的对象,先去调用该类的无参构造器
	gg.visit();

}

int main(){

	test01();

	system("pause");
	return 0;
}
4.4.3 成员函数做友元

friend 返回值类型 类名::成员函数名(形参列表);


class Building;
class goodGay
{
public:

	goodGay();
    //在Building类中声明:friend void goodGay::visit(); 
	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;
}

void test01()
{
	goodGay  gg;
	gg.visit();

}

int main(){
    
	test01();

	system("pause");
	return 0;
}
总结

声明位置:

  • 友元声明以关键字 friend 开始,它只能出现在类定义中。
  • 因为友元不是授权类的成员,所以它不受其所在类的声明区域 public private 和 protected 的影响。
  • 通常我们 选择把所有友元声明组织在一起并放在类头之后.

友元的利弊:

  • 友元不是类成员,但是它可以访问类中的私有成员。
  • 友元的作用在于提高程序的运行效率。但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。

注意事项:

  • 友元关系不能被继承。
  • 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明。
  • 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定 是类 A 的友元,同样要看类中是否有相应的声明

4.5 运算符重载(部分)

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

运算符重载只是一种语法上的方便,也就是说 它只是另一种 函数调用 的方式

运算符重载的两种方式;①类的非静态成员函数 ②非成员函数 ③友元函数

对于内置数据类型的表达式的运算符重载是不可能改变的

不要重载 && 和 ||(可能失去短路特性)

除了赋值号“ = ”外,基类中被重载的操作符都将被派生类继承

4.5.1 加号运算符重载(operator+)

作用:实现两个自定义数据类型相加的运算

案例:成员函数,全局函数实现 + 号运算符重载(两种方法不能同时使用:报重定义)

class Person {
public:
	Person() {};
    
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
    
	//1.成员函数实现 + 号运算符重载(重载为类的非静态成员函数)
	Person operator+(const Person& p) {
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}

public:
	int m_A;
	int m_B;
};

//2.全局函数实现 + 号运算符重载 (重载为非成员函数)
//Person operator+(const Person& p1, const Person& p2) {
//	Person temp(0, 0);
//	temp.m_A = p1.m_A + p2.m_A;
//	temp.m_B = p1.m_B + p2.m_B;
//	return temp;
//}

//3.运算符重载 也可以发生函数重载 
Person operator+(const Person& p2, int val)  
{
	Person temp;
	temp.m_A = p2.m_A + val;
	temp.m_B = p2.m_B + val;
	return temp;
}

void test() {

	Person p1(10, 10);
	Person p2(20, 20);

	//成员函数方式
	Person p3 = p2 + p1;  //本质是 Person p3 = p2.operaor+(p1) 编译器做了一下简化
	cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;

    //全局函数方式 本质是 Person p5 = operator+(p1, p2);
    Person p3 = p1 + p2;
	
    //运算符重载方式
	Person p4 = p3 + 10; //相当于 Person p4 = operator+(p3,10)
	cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;

}

int main() {

	test();

	system("pause");

	return 0;
}

总结1:对于内置的数据类型的表达式的的运算符是不可能改变的

总结2:不要滥用运算符重载

案例二: 将“+”、“-”运算重载为复数类的成员函数

#include <iostream>
using namespace std;

class Complex {	//复数类定义
public:	//外部接口
	Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }	//构造函数
	Complex operator + (const Complex &c2) const;	//运算符+重载成员函数
	Complex operator - (const Complex &c2) const;	//运算符-重载成员函数
	void display() const;	//输出复数
private:	//私有数据成员
	double real;	//复数实部
	double imag;	//复数虚部
};

Complex Complex::operator + (const Complex &c2) const {	//重载运算符函数实现
	return Complex(real + c2.real, imag + c2.imag); //创建一个临时无名对象作为返回值
}

Complex Complex::operator - (const Complex &c2) const {	//重载运算符函数实现
	return Complex(real - c2.real, imag - c2.imag); //创建一个临时无名对象作为返回值
}

void Complex::display() const 
{
	cout << "(" << real << ", " << imag << ")" << endl;
}

int main()	//主函数
{	
	Complex c1(5, 4), c2(2, 10), c3;	//定义复数类的对象
	cout << "c1 = "; c1.display();
	cout << "c2 = "; c2.display();
	c3 = c1 - c2;	//使用重载运算符完成复数减法
	cout << "c3 = c1 - c2 = "; c3.display();
	c3 = c1 + c2;	//使用重载运算符完成复数加法
	cout << "c3 = c1 + c2 = "; c3.display();
    
    system("pause");
	return 0;
}

友元函数进行重载

案例三:将+、-(双目)重载为非成员函数,并将其声明为复数类的友元,两个操作数都是复数类的常引用。
将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是std::ostream引用,右操作数为复数类的常引用,返回std::ostream引用,用以支持下面形式的输出:cout << a << b; 该输出调用的是:operator << (operator << (cout, a), b);见4.5.2

#include <iostream>
using namespace std;

class Complex {	//复数类定义
public:	//外部接口
	Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }	//构造函数
	friend Complex operator + (const Complex &c1, const Complex &c2);	//运算符+重载
	friend Complex operator - (const Complex &c1, const Complex &c2);	//运算符-重载
	friend ostream & operator << (ostream &out, const Complex &c); //运算符<<重载
private:	//私有数据成员
	double real;	//复数实部
	double imag;	//复数虚部
};

Complex operator + (const Complex &c1, const Complex &c2) {
	return Complex(c1.real + c2.real, c1.imag + c2.imag); 
}

Complex operator - (const Complex &c1, const Complex &c2) {
	return Complex(c1.real - c2.real, c1.imag - c2.imag); 
}

ostream & operator << (ostream &out, const Complex &c) {
	out << "(" << c.real << ", " << c.imag << ")";
	return out;
}
4.5.2 左移运算符重载(operator<<)

只能用全局函数实现左移运算符重载

作用:可以输出自定义数据类型

案例:友元函数碰上左移运算符

class Person {
    //全局函数做友元
	friend ostream& operator<<(ostream& out, Person& p);

public:

	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}

	//1.成员函数 实现不了 重载<<运算符 
    
    // 本质是:p.operator << (cout) 简化为 p << cout
	//void operator<<(Person& p){
	//}
    
    // 本质是:p.operator << (cout) 简化为 p << cout, cout 不在左侧
    //void operator<<(cout){
	//}

private:
	int m_A;
	int m_B;
};

//2.全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) { 
    //这行代码能输出的前提是 p.m_A p.m_B是public;
    //如果 p.m_A p.m_B是private,可以使用友元函数来对其进行访问
	out << "a:" << p.m_A << " b:" << p.m_B; 
    //如果没有下面这行代码,且返回值是void类型的话,下面的链式编程就会报错。因为返回值为void, 则cout << p1 结束后就返回void, void << "hello world" 这显然不成立,必须要让他返回ostream对象,又因为ostream对象只能有一个,所以只能采用引用的方式返回ostream对象
	return out;
}

void test() {

	Person p1(10, 20);

    //本质 operator<<(cout, p1) 简化 cout << p1
	cout << p1 << "hello world" << endl; //链式编程
}

int main() {

	test();

	system("pause");

	return 0;
}

总结:重载左移运算符配合友元可以实现输出自定义数据类型

4.5.3 递增运算符重载(operator++)

作用: 通过重载递增运算符,实现自己的整型数据


class MyInteger {

	friend ostream& operator<<(ostream& out, MyInteger myint);

public:
	MyInteger() {
		m_Num = 0;
	}
    
    
	//重置前置++运算符,返回引用是为了一直对一个数据进行递增操作
    //如果返回值类型不是MyInteger&,而是MyInteger的话,返回的是不同的对象,就像浅拷贝那样。那么在执行cout << ++(++a)<< endl; cout << a << endl; 这两句话的时候, a的值是没有实现递增的
	MyInteger& operator++() {
		//先++
		m_Num++;
		//再将自身返回
		return *this; // this是指向MyInterger对象的指针
	}

	//重置后置++运算符
    //MyInteger operator++(int) 中 int 代表占位参数,可以用于区分前置和后置递增
	MyInteger operator++(int) {
		//先返回
		MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
		m_Num++;
		return temp;
	}

private:
	int m_Num;
};


//重载左移运算符
ostream& operator<<(ostream& out, MyInteger myint) {
	out << myint.m_Num;
	return out;
}


//前置++ 先++ 再返回
void test01() {
	MyInteger myInt;
	cout << ++(++myInt) << endl;
	cout << myInt << endl;
}

//后置++ 先返回 再++
void test02() {

	MyInteger myInt;
	cout << myInt++ << endl; //必须重载左移运算符才能够对myInt对象进行输出
	cout << myInt << endl;
}

int main() {

	test01();
	//test02();

	system("pause");

	return 0;
}

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

++i 和 i++ :++i的效率更高。前缀形式少创建了一个临时变量(见上例)

案例二: 操作数是时钟类的对象。实现时间增加1秒钟。

#include <iostream>
using namespace std;

class Clock 
{	//时钟类声明定义
public:	//外部接口
	Clock(int hour = 0, int minute = 0, int second = 0);
	void showTime() const;
	Clock& operator ++ (); //前置单目运算符重载
	Clock operator ++ (int);//后置单目运算符重载
private:	//私有数据成员
	int hour, minute, second;
};

//前置单目运算符重载函数
Clock & Clock::operator ++ () 
{
	second++;
	if (second >= 60) 
    {
		second -= 60;
		minute++;
		if (minute >= 60)
        {
			minute -= 60;
			hour = (hour + 1) % 24;
		}
	}
	return *this;
}

//后置单目运算符重载
Clock Clock::operator ++ (int) 
{
	//注意形参表中的整型参数
	Clock old = *this;
	++(*this);	//调用前置“++”运算符
	return old;
}

int main()
{
	Clock myClock(23, 59, 59);
	cout << "First time output: ";
	myClock.showTime();
	cout << "Show myClock++:    ";
	(myClock++).showTime();
	cout << "Show ++myClock:    ";
	(++myClock).showTime();
	return 0;
}
4.5.4 赋值运算符重载(operator=)

必须用成员函数进行重载

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

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

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

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

  4. 赋值运算符 operator=, 对属性进行值拷贝

  5. 取地址运算符 operator &

  6. const修饰的取地址运算符

    const String* operator&() const
    {
        return this;
    }
    

示例:

class Person{
public:
	int m_Age;
}
void test(){
	Person p1;
	p1.m_Age = 10;
	
	Person p2;
	p2 = p1; //这行语句之所以不出错是因为 编译器默认提供了 构造函数,拷贝构造函数(值拷贝),析构函数,operator=(值拷贝)
	cout << "p2.age =" << p2.age << endl;
}

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。这是需要重载= 运算符

class Person {
public:
    Person(char* name, int age) {
        this->m_Name = new char[strlen(name) + 1];
        strcpy(this->m_Name, name);
        this->m_Age = age;
    }

    ~Person() {
        if (this->m_Name != NULL) {
            delete[] this->m_Name;
            this->m_Name = NULL;
        }
    }

    //重载 = 
    Person& operator=(const Person& p) {
        //先判断原来堆区是否有内容,如果有先释放
        if (this->m_Name != NULL) {
            delete[] this->m_Name;
            this->m_Name = NULL;
        }
        this->m_Name = new char[strlen(p.m_Name) + 1];
        strcpy(this->m_Name, p.m_Name);
        this->m_Age = p.m_Age;
        return *this;
    }

    //重载拷贝构造函数
    Person(const Person& p) {
        this->m_Name = new char[strlen(p.m_Name) + 1];
        strcpy(this->m_Name, p.m_Name);
        this->m_Age = p.m_Age;
    }

    char* m_Name;
    int m_Age;
};

void test() {
    Person p1((char *)"tom", 10);

    Person p2((char *)"Jerry", 20);
    p2 = p1;//由于编译器提供的=默认进行浅拷贝操作,故在调用析构函数时会释放同一片内存空间两次(具体见深浅拷贝一节)。所以需要重载=运算符
    cout << "p1的姓名:" << p1.m_Name << "p1的年龄:" << p1.m_Age << endl;
    cout << "p2的姓名:" << p1.m_Name << "p2的年龄:" << p1.m_Age << endl;


    Person p3((char *)"sfs", 0);
    p3 = p2 = p1; //为了进行该操作,重载=的函数必须返回引用(跟前置++一样)

    Person P4 = p3; //需要重写拷贝构造函数,因为默认的拷贝构造提供的是浅拷贝
}
4.5.5 关系运算符重载(operator==)

还有 > < 这些关系运算符可以重载

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

示例:

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

void test01()
{
	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;
	}
}


int main() {

	test01();

	system("pause");

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

示例:

// 打印输出类
class MyPrint
{
public:
    //重载函数调用运算符
	void operator()(string text)
	{
		cout << text << endl;
	}

};

void test01()
{
	MyPrint myFunc;
    //重载的()操作符 由于使用起来非常类似于函数调用,因此称为仿函数
	myFunc("hello world"); //仿函数 myFunc 不是函数名, 而是对象名。本质是一个对象, 又称为函数对象
   	//void test(string str){
    //	cout << str << endl;
	//}
    //test("hello world");//普通函数,test为函数名
}



//仿函数案例二
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();

	system("pause");

	return 0;
}
4.5.7 重载指针(*,->)运算符
class Person{
public:
    Person(int age){
        cout << "Person有参构造被调用" << endl;
        this->m_Age = age;
    }
     
    void showAge(){
        cout <<"年龄为:" << m_Age << endl;
    }

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

    int m_Age;
};

class SmartPointer{
public:
    SmartPointer(Person* person){
        this->m_person = person;
    }

    //重载 -> 运算符
    Person* operator->(){
        return this->m_person;
    }

    //重载 * 运算符
    Person& operator*(){
        return *m_person;
    }

    ~SmartPointer(){
        if(this->m_person){
            delete this->m_person;
            this->m_person = NULL;
        }
    }

private:
    Person* m_person;
};


void test(){
    //为了避免 忘记手动释放堆区开辟的内存,采用智能指针
    // Person* p = new Person(18);
    // (*p).showAge();
    // p->showAge();
    // delete p;

    //利用智能指针 管理 new 出来的person的释放操作
    //因为智能指针所new 出来的对象 存放在栈区,在调用结束后释放
    SmartPointer sp(new Person(18));
    //需要重载 -> * 运算符
    sp->showAge();  // 本质是 sp -> -> showAge(); 编译器简化为 sp-> showAge();
    (*sp).showAge();
}

int main(){
    test();

    system("pause");
    return 0;
}
案例
1.封装 Array
2. 封装 string
总结
  • = () [] * 和 -> 操作符只能通过 成员函数进行重载
  • **<< 和 >>**只能通过 全局函数配合 友元函数进行重载
  • 不要重载 && 和 || 操作符,因为无法实现短路规则

4.6 继承

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

4.6.1 继承的基本语法

例如我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同

接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处

普通实现:

//Java页面
class Java 
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java,Python,C++...(公共分类列表)" << endl;
	}
	void content()
	{
		cout << "JAVA学科视频" << endl;
	}
};
//Python页面
class Python
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java,Python,C++...(公共分类列表)" << endl;
	}
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};
//C++页面
class CPP 
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}
	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java,Python,C++...(公共分类列表)" << endl;
	}
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};

void test01()
{
	//Java页面
	cout << "Java下载视频页面如下: " << endl;
	Java ja;
	ja.header();
	ja.footer();
	ja.left();
	ja.content();
	cout << "--------------------" << endl;

	//Python页面
	cout << "Python下载视频页面如下: " << endl;
	Python py;
	py.header();
	py.footer();
	py.left();
	py.content();
	cout << "--------------------" << endl;

	//C++页面
	cout << "C++下载视频页面如下: " << endl;
	CPP cp;
	cp.header();
	cp.footer();
	cp.left();
	cp.content();

}

int main() {

	test01();

	system("pause");

	return 0;
}

继承实现:

//公共页面
class BasePage
{
public:
	void header()
	{
		cout << "首页、公开课、登录、注册...(公共头部)" << endl;
	}

	void footer()
	{
		cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
	}
	void left()
	{
		cout << "Java,Python,C++...(公共分类列表)" << endl;
	}

};

//Java页面
class Java : public BasePage
{
public:
	void content()
	{
		cout << "JAVA学科视频" << endl;
	}
};
//Python页面
class Python : public BasePage
{
public:
	void content()
	{
		cout << "Python学科视频" << endl;
	}
};
//C++页面
class CPP : public BasePage
{
public:
	void content()
	{
		cout << "C++学科视频" << endl;
	}
};

void test01()
{
	//Java页面
	cout << "Java下载视频页面如下: " << endl;
	Java ja;
	ja.header();
	ja.footer();
	ja.left();
	ja.content();
	cout << "--------------------" << endl;

	//Python页面
	cout << "Python下载视频页面如下: " << endl;
	Python py;
	py.header();
	py.footer();
	py.left();
	py.content();
	cout << "--------------------" << endl;

	//C++页面
	cout << "C++下载视频页面如下: " << endl;
	CPP cp;
	cp.header();
	cp.footer();
	cp.left();
	cp.content();


}

int main() {

	test01();

	system("pause");

	return 0;
}

总结:

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

class A : public B;

A 类称为子类 或 派生类

B 类称为父类 或 基类

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

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

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

4.6.2 继承方式

继承的语法:class 子类 : 继承方式 父类

继承方式一共有三种:

  • 公共继承

    • 基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。

    • 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。

    • 通过派生类的对象只能访问基类的public成员。

  • 保护继承

    • 基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问

    • 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员

    • 通过派生类的对象不能直接访问基类中的任何成员

  • 私有继承

    • 基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。

    • 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。

    • 通过派生类的对象不能直接访问基类中的任何成员。

如果类中属性未使用访问修饰符 access-specifier,则默认为 private。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例

公有继承

class Base1
{
public: 
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

//1. 公共继承
class Son1 :public Base1
{
public:
	void func()
	{
		m_A; //可访问 public权限,且在子类中变为 public
		m_B; //可访问 protected权限,且在子类中变为 protected
		//m_C; //不可访问 基类中的 private 属性
	}
};

void myClass()
{
	Son1 s1;
	s1.m_A; //在 Son1中,m_A是public 权限,类外可以访问
    //s.m_B;//在 Son1中,m_B是protected 权限,类外不可以访问
}

保护继承:

//2. 保护继承
class Base2
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};
class Son2:protected Base2
{
public:
	void func()
	{
		m_A; //可访问 public权限,且在子类中变为 protected
		m_B; //可访问 protected权限,且在子类中变为 protected
		//m_C; //父类中私有成员,不可访问
	}
};
void myClass2()
{
	Son2 s;
	//s.m_A; //子类中protected 权限无法访问
    //s.m_B; //子类中protected 权限无法访问
    //s.m_C; //子类无法访问父类中的私有属性
}

私有继承:

//3. 私有继承
class Base3
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C;
};

class Son3:private Base3
{
public:
	void func()
	{
		m_A; //可访问 public权限,且在子类中变为 private
		m_B; //可访问 protected权限,且在子类中变为 private
		//m_C; //父类中私有成员,不可访问
	}
};
class GrandSon3 :public Son3
{
public:
	void func()
	{
		//Son3是私有继承,所以继承Son3的属性(都是 private)在GrandSon3中都无法访问到
		//m_A;
		//m_B;
		//m_C;
	}
};

void myClass3()
{
	Son3 s;
	//s.m_A; //子类中private 权限无法访问
    //s.m_B; //子类中privae 权限无法访问
    //s.m_C; //子类无法访问父类中的私有属性
}

4.6.3 继承中的对象模型

**问题:**从父类继承过来的成员,哪些属于子类对象中?

示例:

class Base
{
public:
	int m_A;
protected:
	int m_B;
private:
	int m_C; //父类中的私有属性,子类访问不到,是由编译器给隐藏了,所以计算子类大小为16(内存对齐)
};

//公共继承
class Son :public Base
{
public:
	int m_D;
};

void test01()
{
	cout << "sizeof Son = " << sizeof(Son) << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}

利用工具查看:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

打开工具窗口后,定位到当前CPP文件的盘符

然后输入: cl /d1 reportSingleClassLayout查看的类名 所属文件名

效果如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论: 父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到

4.6.4 单继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数

问题:父类和子类的构造和析构顺序是谁先谁后?

先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反(联想栈的先进后出)

如果某个类有类成员,则先调用该类成员的构造函数,再调用该类的构造函数

示例:

class Base {
public:
	Base(){
		cout << "Base构造函数!" << endl;
	}
	~Base(){
		cout << "Base析构函数!" << endl;
	}
};

class Other {
public:
	Other(){
		cout << "Other构造函数!" << endl;
	}
	~Other(){
		cout << "Other析构函数!" << endl;
	}
};

class Son : public Base{
public:
	Son(){
		cout << "Son构造函数!" << endl;
	}
	~Son(){
		cout << "Son析构函数!" << endl;
	}
	Other other;
};


void test01(){
	//创建子类对象:先调用父类构造函数,再调用 子类对象成员构造函数,再调用子类构造函数,析构顺序与构造相反(联想栈的先进后出)
	Son s;
}

int main() {

	test01();

	system("pause");

	return 0;
}

总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

利用初始化列表,调用父类其他构造函数

class Base2{
public:
    Base2(int a){
        this->m_A = a;
        cout << "Base2的构造函数调用" << endl;
    }
    int m_A;
}

class Son2:public Base2{
public:
    //子类会调用父类的默认构造函数,但是父类没有提供,采用初始化列表,显示调用Base2(int a)
    //Son2(){
    //    cout << "Son2的构造函数调用" << endl;
    //}
    
    //默认参数,给父类传值
    Son2(int a = 100):Base2(a){ //利用初始化列表语法,显示调用父类中的其他构造函数
        cou << "Son2的构造函数调用" << endl;
    }
}

void test(){
    Son2 s;
    cout << s.m_A << endl;
}

父类中的 构造函数,析构函数,拷贝构造函数,operator= 是不会被子类继承下去的

继承时的构造函数和拷贝构造函数
继承时的构造函数
  • 基类的构造函数不被继承,派生类中需要声明自己的构造函数。
  • 定义构造函数时,只需要对本类中新增成员进行初始化。对继承来的基类成员的初始化,自动调用基类构造函数完成。
  • 派生类的构造函数需要给基类的构造函数传递参数

一. 单继承时的构造函数(见 4.6.4)

定义:

派生类名::派生类名(基类所需的形参,本类成员所需的形参):基类名(参数表)
{
本类成员初始化赋值语句;
};

案例:

先调用基类的构造函数,再调用派生类对象成员的构造函数(这个案例没有体现),再调用派生类构造函数

class B {
public:
	B();
	B(int i);
	~B();
	void print() const;
private:
	int b;
};

B::B() {
	b=0;
	cout << "B's default constructor called." << endl;
}

B::B(int i) {
	b=i;
	cout << "B's constructor called." << endl;
}

B::~B() {
	cout << "B's destructor called." << endl;
}

void B::print() const {
	cout << b << endl;
}

class C: public B //公有继承{ 
public:
	C();
	C(int i, int j);
	~C();
	void print() const;
private:
	int c;
};

C::C() {
	c = 0;
	cout << "C's default constructor called." << endl;
}

C::C(int i,int j): B(i){ //利用初始化列表,显示调用父类有参构造函数
	c = j;
	cout << "C's constructor called." << endl;
}

C::~C() {
	cout << "C's destructor called." << endl;
}

void C::print() const{
	B::print();
	cout << c << endl;
}

int main() {
	C obj(5, 6);
	obj.print();
	return 0;
}

二.多继承时的构造函数

派生类名::派生类名(参数表):基类名1(基类1初始化参数表), 基类名2(基类2初始化参数表), …基类名n(基类n初始化参数表)
{
本类成员初始化赋值语句;
};

多继承派生类构造函数举例

按声明顺序,调用基类的构造函数;

再按声明顺序,调用派生类对象成员的构造函数;

再调用派生类的构造函数

class Base1 //基类Base1,构造函数有参数
{		
public:
	Base1(int i) { cout << "Constructing Base1 " << i << endl; }
};

class Base2 //基类Base2,构造函数有参数
{	
public:
	Base2(int j) { cout << "Constructing Base2 " << j << endl; }
};

class Base3 //基类Base3,构造函数无参数
{	
public:
	Base3() { cout << "Constructing Base3 *" << endl; }
};

class Derived: public Base2, public Base1, public Base3 //派生新类Derived,注意基类名的顺序
{
public:	//派生类的公有成员
	Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c), Base2(b)
	{
        cout << "Constructing Derived";
    }
	//注意基类名的个数与顺序,//注意成员对象名的个数与顺序
private:	//派生类的私有成员对象
	Base1 member1;
	Base2 member2;
	Base3 member3;
};

int main()
{
	Derived obj(1, 2, 3, 4);
	return 0;
}

三. 派生类与基类的构造函数

当基类中声明有缺省构造函数或未声明构造函数时,派生类构造函数可以不向基类构造函数传递参数,也可以不声明,构造派生类的对象时,基类的缺省构造函数将被调用。

当需要执行基类中带形参的构造函数来初始化基类数据时,派生类构造函数应在初始化列表中为基类构造函数提供参数。

四. 多继承且有内嵌对象时的构造函数(4.2.7)

派生类名::派生类名(形参表):基类名1(参数), 基类名2(参数), …基类名n(参数),新增成员对象的初始化
{
本类成员初始化赋值语句;
};

重点总结:

构造函数的执行顺序(先父后子,同类型按定义顺序)

​ 1. 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
​ 2. 对成员对象(一个类的成员变量如果是另一个类的对象,就称之为“成员对象)进行初始化,初始化顺序按照它们在类中声明的顺序。
​ 3.执行派生类的构造函数中的内容(即调用派生类构造函数)

(最先对基类成员对象进行初始),先调用基类构造函数,再对派生类成员对象进行初始化,最后执行派生类构造函数【析构函数调用顺序正好相反,联想栈的结构】

某类作为派生类时,该类对象初始化一定会调用基类的构造函数

注意:如果没有给基类默认的无参构造器,而在派生类中创建了有参构造器的话,必须在基类中创建无参构造器。

因为创建子类对象时,先要调用父类的构造器

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

  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数(无参构造,有参构造)

拷贝构造函数
  1. 若建立派生类对象时没有编写拷贝构造函数,编译器会生成一个隐含的拷贝构造函数,该函数先调用基类的拷贝构造函数,再为派生类新增的成员对象执行拷贝。
  2. 若编写派生类的拷贝构造函数,则需要为基类相应的拷贝构造函数传递参数。例如:

​ C::C(C &c1): B(c1) {…}

拷贝构造函数实例

当用类的一个对象去初始化该类的另一个对象时系统自动调用拷贝构造函数实现拷贝赋值。

  • 如果派生类没有为基类相应的拷贝构造函数传递参数,则用类的一个对象去初始化该类的另一个对象时,先调用基类的构造函数,后调用派生类的拷贝构造函数

  • 如果为基类相应的拷贝构造函数传递参数,则用类的一个对象去初始化该类的另一个对象时,先调用基类的拷贝构造函数,后调用派生类的拷贝构造函数

class A
{
    public:
        A() { cout << "A default constructor" << endl; }
        A(A&) { cout << "A copy constructor" << endl; }
};

class B : public A
{
    public:
        B() { cout << "B default constructor" << endl; }
        B(B &b) { cout << "B copy constructor" << endl; }
        // B(B &b) : A(b) { cout << "B copy constructor" << endl; }
};

 int main()
{
    B b;   //会先调用父类的构造函数,后调用子类的构造函数 
    B c = b; //到底调不调用父类的拷贝构造函数要具体分析

    system("pause");
    return 0; 
}

拷贝构造函数的调用时机(4.2.3)

  • 当用类的一个对象去初始化该类的另一个对象时系统自动调用拷贝构造函数实现拷贝赋值。

    class Point {
    public:
    	Point(int xx=0, int yy=0) { x = xx; y = yy; }
    	Point(Point& p);
    	int getX() { return x; }
    	int getY() { return y; }
    private:
    	int x, y;
    };
    
    Point::Point (Point& p) {
      x = p.x;
      y = p.y;
      cout << "Calling the copy constructor " 	<< endl;
    }
    
    int main() {
      Point a(1,2);
      Point b = a; //拷贝构造函数被调用
      cout << b.getX() << endl;
    }
    
  • 函数的形参为类对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数

    void fun1(Point p) {
      cout << p.getX() << endl;
    } 
    int main() {
      Point a(1, 2);
      fun1(a); //调用拷贝构造函数
      return 0;
    }     
    
  • 当函数的返回值是类对象时,系统自动调用拷贝构造函数

    Point fun2() {
      Point a(1, 2);
      return a; //调用拷贝构造函数
    }
    int main() {
      Point b;
      b = fun2();
      return 0; 
    }
    
继承时的析构函数
  • 析构函数也不被继承,派生类自行声明

  • 声明方法与一般(无继承关系时)类的析构函数相同。

  • 不需要显式地调用基类的析构函数,系统会自动隐式调用。

  • 析构函数的调用次序与构造函数相反(参考栈的结构,先进后出)

      	即先调用派生类析构函数,再调用派生类对象成员的析构函数,再调用基类析构函数,最后调用基类对象成员的析构函数
    

派生类析构函数举例:

class Base1//基类Base1,构造函数有参数
{	
public:
	Base1(int i) { cout << "Constructing Base1 " << i << endl; }
	~Base1() { cout << "Destructing Base1" << endl; }
};

class Base2 //基类Base2,构造函数有参数
{	
public:
	Base2(int j) { cout << "Constructing Base2 " << j << endl; }
	~Base2() { cout << "Destructing Base2" << endl; }
};

class Base3 //基类Base3,构造函数无参数
{	
public:
	Base3() { cout << "Constructing Base3 *" << endl; }
	~Base3() { cout << "Destructing Base3" << endl; }
};

class Derived: public Base2, public Base1, public Base3 //派生新类Derived,注意基类名的顺序
{

public:	//派生类的公有成员
	Derived(int a, int b, int c, int d): Base1(a), member2(d), member1(c), Base2(b) 
    {
        cout << "Constructing Derived" << endl;
    }
	//注意基类名的个数与顺序,注意成员对象名的个数与顺序
private:	//派生类的私有成员对象
	Base1 member1;
	Base2 member2;
	Base3 member3;
};

int main() {
	Derived obj(1, 2, 3, 4);
	return 0;
}
4.6.7 继承同名成员处理方式

同名隐藏规则

当派生类与基类中有相同成员时:

  • 若未强行指名,则通过派生类对象使用的是派生类中的同名成员。
  • 如要通过派生类对象访问基类中被隐藏的同名成员,应使用基类名限定。
  • 当子类重新定义了父类中的同名函数,子类的成员函数会 隐藏掉父类中所有重载版本的同名函数。
class Base1 //定义基类Base1
{	
public:
	int var;
	void fun() { cout << " Base1 fun()" << endl; }
    void fun(int a){cout << "Base1 fun(int a)"}
};

class Base2 //定义基类Base2
{	
public:
	int var;
	void fun() { cout << "Base2 fun()" << endl; }
};

class Derived: public Base1, public Base2  //定义派生类Derived
{
public:
	int var;	//同名数据成员
	void fun() { cout << "Derived fun()" << endl; }	//同名函数成员
};

int main() 
{
	Derived d;
	Derived *p = &d;

	d.var = 1;	//对象名.成员名标识
	d.fun();	//访问Derived类成员
    
    //当子类重新定义了父类中的同名函数,子类的成员函数会 隐藏掉父类中所有重载版本的同名函数。可以利用作用域显示指定调用
    //d.fun(10); //报错,并没有调用父类中重载版本的fun(int a)函数,而是在自己类中寻找 fun(int a)
    d.Base1::fun(10);
	
	d.Base1::var = 2;	//作用域分辨符标识
	d.Base1::fun();	//访问Base1基类成员
	
	p->Base2::var = 3;	//作用域分辨符标识
	p->Base2::fun();	//访问Base2基类成员

	return 0;
}

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

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员

​ ① 类名限定 需要加作用域 c1.A::f() 或 c1.B::f()

​ ② 同名隐藏 在C 中声明一个同名成员函数f(),f()再根据需要调用 A::f() 或 B::f()

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

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

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

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

示例:

class Base {
public:
	static void func()
	{
		cout << "Base - static void func()" << endl;
	}
	static void func(int a)
	{
		cout << "Base - static void func(int a)" << endl;
	}

	static int m_A;
};

int Base::m_A = 100;

class Son : public Base {
public:
	static void func()
	{
		cout << "Son - static void func()" << endl;
	}
	static int m_A;
};

int Son::m_A = 200;

//同名成员属性
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;
}

//同名成员函数
void test02()
{
	//通过对象访问
	cout << "通过对象访问: " << endl;
	Son s;
	s.func();
	s.Base::func();
    s.Base::func(100);

	cout << "通过类名访问: " << endl;
	Son::func();
	Son::Base::func();
	//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
	Son::Base::func(100);
}
int main() {

	//test01();
	test02();

	system("pause");

	return 0;
}

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)

4.6.9 多继承语法

C++允许一个类继承多个类

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

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

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

示例:

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

class Base2 {
public:
	Base2(){
		m_A = 200;  //开始是m_B 不会出问题,但是改为mA就会出现不明确
	}
public:
	int m_A;
};

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


//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
	Son s;
	cout << "sizeof Son = " << sizeof(s) << endl;
	cout << s.Base1::m_A << endl;
	cout << s.Base2::m_A << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}

总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域

多继承中二义性问题

在多继承时,基类与派生类之间,或基类之间出现同名成员时,将出现访问时的二义性(不确定性)——采用虚函数或同名隐藏规则来解决。
当派生类从多个基类派生,而这些基类又从同一个基类派生(菱形继承),则在访问此共同基类中的成员时,将产生二义性——采用虚基类来解决。

示例:

class Base {
public:
	Base()
	{
		m_A = 100;
	}

	void func()
	{
		cout << "Base - func()调用" << endl;
	}

	void func(int a)
	{
		cout << "Base - func(int a)调用" << endl;
	}

public:
	int m_A;
};


class Son : public Base {
public:
	Son()
	{
		m_A = 200;
	}

	//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
	//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
	void func()
	{
		cout << "Son - func()调用" << endl;
	}
public:
	int m_A;
};

void test01()
{
	Son s;

	cout << "Son下的m_A = " << s.m_A << endl;
	cout << "Base下的m_A = " << s.Base::m_A << endl;

	s.func();
	s.Base::func();
	s.Base::func(10);

}
int main() {

	test01();

	system("pause");
	return EXIT_SUCCESS;
}

总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
4.6.10 菱形继承(虚继承)

回顾4.4.7

菱形继承概念:

​ 两个类有公共的父类,和共同的子类。这种继承被称为菱形继承,或者钻石继承

​ 当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生二义性 ——采用虚基类来解决。

典型的菱形继承案例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
    
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
  3. 这时,我们可以让羊和驼继承虚基类动物(在继承前用virtual修饰动物),然后让草泥马继承羊和驼,

虚基类

  • 作用:

​ 用于有共同基类的场合

  • 声明:

​ 以virtual修饰虚基类 例:class B1 : virtual public B

  • 作用:

​ 主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题.

​ 为最远的派生类提供唯一的基类成员,而不重复产生多次拷贝

  • 注意:

​ 在第一级继承时就要将共同基类设计为虚基类(即羊和驼继承的是虚基类)

示例:

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;
    
    //当发生虚继承后,sheep和tuo类中,继承了一个 vbptr指针(虚基类指针) 指向了一个 虚基类表 vbtable
    //虚基类表中记录了 偏移量,通过偏移量 可以找到唯一的一个 m_Age(见4.6.11 不同继承情况下的内存布局)
}


int main() 
{

	test01();

	system("pause");

	return 0;
}

总结:

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

虚基类及其派生类构造函数

  • 建立对象时所指定的类称为最(远)派生类。

  • 虚基类的成员是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。

  • 在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中给出对虚基类的构造函数的调用。如果未列出,则表示调用该虚基类的默认构造函数。

  • 在建立对象时,只有最派生类的构造函数调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用被忽略。

有虚函数和没有虚函数的区别:

爷爷类构造函数的调用的次数不同

有虚函数:创建孙子类对象时,先调用爷爷类(虚基类)的构造函数,再调用父类构造函数1,2(按照初始化的先后),然后调用本类构造函数

没有虚函数:创建孙子类对象时,先调用爷爷构造函数,再调用父类构造函数1,再调用爷爷类构造函数,再调用父类构造函数2(1,2的调用按照孙子类构造函数初始化父类构造函数的顺序),最后调用子类的构造函数

有虚基类时的构造函数举例

class Base0 //定义基类Base0
{
public:
	Base0(int var) : var0(var) { cout << "Base0()" << endl; }
	int var0;
	void fun0() { cout << "Member of Base0" << endl; }
};

class Base1: virtual public Base0 //定义派生类Base1
{	
public:	//新增外部接口
	Base1(int var) : Base0(var) { cout << "Base1()" << endl; }
	int var1;
};

class Base2: virtual public Base0 	//定义派生类Base2
{
public:	//新增外部接口
	Base2(int var) : Base0(var) { cout << "Base2()"<< endl; }
	int var2;
};

class Derived: public Base1, public Base2 //定义派生类Derived
{	
public:	//新增外部接口
    // 如果没有定义虚基类 Derived(int var) : Base1(var), Base2(var){}
	Derived(int var) : Base0(var), Base1(var), Base2(var) { }
	int var;
	void fun() { cout << "Member of Derived" << endl; }
};

int main()  //程序主函数
{	
    //先调用父类的有参构造器,后调用子类的有参构造器
	Derived d(1);	//定义Derived类对象d
	d.var0 = 2;	//直接访问虚基类的数据成员
	d.fun0();	//直接访问虚基类的函数成员
    
    system("pause");
	return 0;
}
类型兼容规则

一个公有派生类的对象在使用上可以被当作基类的对象,反之则禁止。具体表现在:
派生类的对象可以隐含转换为基类对象。
派生类的对象可以初始化基类的引用。
派生类的指针可以隐含转换为基类的指针。

通过基类对象名、指针只能使用从基类继承的成员

#include <iostream>
using namespace std;

class Base1 { //基类Base1定义
public:
	void display() const {
		cout << "Base1::display()" << endl;
	}
};

class Base2: public Base1 { //公有派生类Base2定义
public:
	void display() const {
		cout << "Base2::display()" << endl;
	}
};

class Derived: public Base2 { //公有派生类Derived定义
public:
	void display() const {
		cout << "Derived::display()" << endl;
	}
};

void fun(Base1 *ptr) { //参数为指向基类对象的指针
	ptr->display();	//"对象指针->成员名"
}

int main() {	//主函数
	Base1 base1;	//声明Base1类对象
	Base2 base2;	//声明Base2类对象
	Derived derived;	//声明Derived类对象

	 //用Base1对象的指针调用fun函数
	fun(&base1);	//Base1::display()
	//用Base2对象的指针调用fun函数
	fun(&base2);	//Base1::display()
	//用Derived对象的指针调用fun函数
    fun(&derived);	//Base1::display()

	return 0;
}
4.6.11组合与继承,虚函数(虚函数先看4.7.1)

组合与继承:通过已有类来构造新类的两种基本方式
组合:B类中存在一个A类型的内嵌对象
有一个(has-a)关系:表明每个B类型对象“有一个” A类型对象
A类型对象与B类型对象是部分与整体关系
B类型的接口不会直接作为A类型的接口

派生类对象的内存布局

因编译器而异
内存布局应使类型兼容规则(4.6.2下)便于实现
基类指针,无论其指向基类对象,还是派生类对象,通过它来访问一个基类中定义的数据成员,都可以用相同的步骤

不同继承情况下的内存布局

单继承:基类数据在前,派生类新增数据在后
多继承:各基类数据按顺序在前,派生类新增数据在后
虚继承:需要增加指针,间接访虚基类数据

单继承:Derived类型指针pd转换为Base类型指针时,地址不需要改变

多继承:Derived类型指针pd转换为Base2类型指针时,原地址需要增加一个偏移量

​ 这个偏移量通过打印pb1和pb2就可以得出了

虚拟继承:通过指针(虚基类指针:virtual base pointer)间接访问虚基类的数据成员

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虚基类指针(pb1,pb2 )指向虚基类表(virtual base table),虚基类表中记录了偏移量。通过虚基类指针和偏移量找到唯一的数据。

基类向派生的转换static_cast

基类向派生类的转换
基类向派生类的转换
基类指针可以转换为派生类指针
基类引用可以转换为派生类引用
需要用static_cast显式转换

Base *pb = new Derived();  //子转父
Derived *pd = static_cast<Derived *>(pb);  //父转子,不安全
	
Derived d;
Base &rb = d;
Derived &rb = static_cast<Derived &>(rb);	//子转父

类型转换时的注意事项

基类对象一般无法被显式转换为派生类对象

  • 对象到对象的转换,需要调用构造函数创建新的对象
  • 派生类的拷贝构造函数无法接受基类对象作为参数

执行基类向派生类的转换时,一定要确保被转换的指针和引用所指向或引用的对象符合转换的目的类型:

  • 对于Derived *pd = static_cast<Derived *>(pb);

    一定要保证pb所指向的对象具有Derived类型,或者是Derived类型的派生类。

如果A类型是B类型的虚拟基类,A类型指针无法通过static_cast隐含转换为B类型的指针

  • 可以结合虚继承情况下的对象内存布局,思考为什么不允许这种转换

void指针参加的转换,可能导致不可预期的后果:

  • 例:(Base2是Derived的第二个公共基类)
    Derived *pd = new Derived();
    void *pv = pd; //将Derived指针转换为void指针
    Base2 *pb = static_cast<Base2 *>(pv);
    转换后pb与pd有相同的地址,而正常的转换下应有一个偏移量
    结论:有void指针参与的转换,兼容性规则不适用

更安全更灵活的基类向派生类转换方式——dynamic_cast,将在下一讲介绍

dynamic_cast的使用(见第六章)

语法形式
dynamic_cast<目的类型>(表达式)

功能
将基类指针转换为派生类指针,将基类引用转换为派生类引用;
转换是有条件的:

​ 如果指针(或引用)所指对象的实际类型与转换的目的类型兼容,则转换成功进行;
​ 否则如执行的是指针类型的转换,则得到空指针;如执行的是引用类型的转换,则抛出异常。

class Base {
public:
	virtual void fun1() { cout << "Base::fun1()" << endl; }
	virtual ~Base() { }
};
class Derived1: public Base {
public:
	virtual void fun1() { cout << "Derived1::fun1()" << endl; }
	virtual void fun2() { cout << "Derived1::fun2()" << endl; }
};
class Derived2: public Derived1 {
public:
	virtual void fun1() { cout << "Derived2::fun1()" << endl; }
	virtual void fun2() { cout << "Derived2::fun2()" << endl; }
}; 
void fun(Base *b) {
	b->fun1();
	//尝试将b转换为Derived1指针
	Derived1 *d = dynamic_cast<Derived1 *>(b);
	//判断转换是否成功
	if (d != 0) d->fun2();
}
int main() {
	Base b;
	fun(&b);
	Derived1 d1;
	fun(&d1);
	Derived2 d2;
	fun(&d2);
	return 0;
}
typeid的使用

语法形式
typeid ( 表达式 )
typeid ( 类型说明符 )
功能:
获得表达式或类型说明符的类型信息
表达式有多态类型时,会被求值,并得到动态类型信息;
否则,表达式不被求值,只能得到静态的类型信息。
类型信息用type_info对象表示:
type_info是typeinfo头文件中声明的类;
typeid的结果是type_info类型的常引用;
可以用type_info的重载的“==”、“!=”操作符比较两类型的异同;
type_info的name成员函数返回类型名称,类型为const char *。

class Base {
public:
	virtual ~Base() { }
};

class Derived: public Base { };
void fun(Base *b) {
	//得到表示b和*b类型信息的对象
	const type_info &info1 = typeid(b);
	const type_info &info2 = typeid(*b);
	cout << "typeid(b): " << info1.name() << endl;
	cout << "typeid(*b): " << info2.name() << endl;
	if (info2 == typeid(Base)) //判断*b是否为Base类型
		cout << "A base class!" << endl;
}

int main() {
	Base b;
	fun(&b);
	Derived d;
	fun(&d);
	return 0;
}

虚函数动态绑定的实现原理

动态选择被执行的函数
函数的调用,需要通过函数代码的入口地址
把函数入口地址作为变量,在不同情况下赋予不同的值,通过该变量调用函数,就可动态选择被执行的 函数

虚表:

​ 每个多态类有一个虚表(virtual table)
​ 虚表中有当前类的各个虚函数的入口地址
​ 每个对象有一个指向当前类的虚表的指针(虚指针vptr)

动态绑定的实现:

​ 构造函数中为对象的虚指针(vptr)赋值
​ 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
​ 通过该入口地址调用虚函数

类型转换时的注意事项

(1)基类对象一般无法被显式转换为派生类对象(跟java类似)

​ 对象到对象的转换,需要调用构造函数创建新的对象

​ 派生类的拷贝构造函数无法接受基类对象作为参数

(2)执行基类向派生类的转换时,一定要确保被转换的指针和引用所指向或引用的对象符合转换的目的类型:

​ 对于Derived *pd = static_cast<Derived *>(pb);

​ 一定要保证pb所指向的对象具有Derived类型,或者是Derived类型的派生类

(3)如果A类型是B类型的虚拟基类,A类型指针无法通过static_cast隐含转换为B类型的指针

(4)void指针参加的转换,可能导致不可预期的后果

(5)使用dynamic_cast(见前面)

4.7 多态

多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。

多态的实现:

  • 函数重载(静态多态)
  • 运算符重载(静态多态)
  • 虚函数(动态多态)
4.7.1 静态多态和动态多态

静态多态和动态多态的区别:函数地址是早绑定(静态联编)还是晚绑定(动态联编)

如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。

如果函数的调用地址不能编译期间确定,而需要在运行时才能决定,这就属于晚绑定(动态多态)

静态多态案例:

class Animal{
public:
    //采用动态绑定,在该函数前加上关键字 virtual 即可。即 virtual void speak(){}
    void speak(){
        cout << "动物在说话" << endl;
    }
};

class Cat : public Animal{
public:
    void speak(){
        cout << "猫在说话" << endl;
    }
};

//对于有父子关系的两个类,指针或者引用是可以直接转换的
void doSpeak(Animal& animal){ //Animal& animal = cat;
    //如果地址早就绑定好了,属于静态联编
    //如果想要调用"猫在说话",这个时候地址就不能早绑定好,而是在运行阶段再去绑定函数地址,属于地址晚绑定,叫动态绑定,要使用虚函数
    animal.speak();
}

void test(){
    cout << sizeof(Animal) << endl; //4 指针(没有virtual是成员函数,不占类的空间)
}

//利用指针的偏移调用 函数
void test2(){
    Animal * animal = new Cat;
    //animal-> speak();
    //*(int *)animal 解引用到虚函数表中
    //*(int *)*(int *)animal  解引用到函数speak (因为存放的是函数的入口地址,在VS32位下,指针类型为4字节大小,故用 int * 强转)
    
    //调用猫说话
    ((void(*)())(*(int *)*(int *)animal))();
}

int main() {

	Cat cat;
    doSpeak(cat);

	system("pause");

	return 0;
}

动态多态产生条件:

  • 有继承关系

  • 父类中有虚函数,子类重写(函数返回值类型相同,函数名相同,形参列表相同)父类中的虚函数

    重载:函数名相同,形参列表不同

  • 父类指针或引用 指向子类对象

动态绑定内存结构分析见4.6.11:虚函数动态绑定实现原理

如果子类没有重写父类中的虚函数,默认继承父类中的虚函数。用父类指针或引用指向子类对象,调用的是父类中的虚函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果子类重写了父类中的虚函数,尽管用父类指针或引用指向子类对象,调用的是子类重写的虚函数,从而实现动态绑定(本质是覆盖了 vftable中虚函数的地址)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

多态原理

  • 父类写了虚函数(virtual)后,类内部结构发生改变,多了一个 vfptr
  • vfptr 虚函数表指针 ===》 指向 vftable 虚函数表
  • 虚函数表内部记录着 虚函数的入口地址
  • 当父类指针或引用指向子类对象时,发生多态,调用的时候从虚函数中找到函数入口地址
虚函数实现动态绑定

虚函数

函数原型前面加上virtual关键字,变成虚函数,有点像java中的abstract方法,实现动态绑定

是非静态的成员函数

virtual 只用来说明类声明中的原型,不能用在函数实现时。

具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数
本质:不是重载声明而是覆盖。

调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。

class Base1 //基类Base1定义
{ 
public:
	virtual void display() const;	//虚函数
};

void Base1::display() const 
{
	cout << "Base1::display()" << endl;
}

class Base2: public Base1 //公有派生类Base2定义
{ 
public:
	void display() const;	//覆盖基类的虚函数
};

void Base2::display() const 
{
	cout << "Base2::display()" << endl;
}

//公有派生类Derived定义
class Derived: public Base2 
{ 
public:
	void display() const; //覆盖基类的虚函数
};

void Derived::display() const {
	cout << "Derived::display()" << endl;
}

//参数为指向基类对象的指针
void fun(Base1 *ptr) 
{ 
	ptr->display();	//"对象指针->成员名"
}

int main() //主函数
{	
	Base1 base1;	//定义Base1类对象
	Base2 base2;	//定义Base2类对象
	Derived derived;	//定义Derived类对象
    //动态绑定
	fun(&base1);	//用Base1对象的指针调用fun函数
	fun(&base2);	//用Base2对象的指针调用fun函数
	fun(&derived);	//用Derived对象的指针调用fun函数
	return 0;
}

总结:

若基类中有虚函数(即该函数用 virtual 来修饰),派生类的同名函数将覆盖基类中的该虚函数,创建子类对象调用该方法时,不会调用基类中的虚函数,而是调用子类中的同名函数,从而实现动态绑定

若基类中的函数不是基函数,则派生类的同名函数将不会覆盖基类中的该函数,创建子类对象调用该方法时,不会调用子类中的同名函数,而是调用基类中的函数(静态绑定)

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

多态分为两类

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

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

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

下面通过案例进行讲解多态

class Animal
{
public:
	//Speak函数就是虚函数
	//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat :public Animal
{
public:
	void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal
{
public:

	void speak()
	{
		cout << "小狗在说话" << endl;
	}

};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编

void DoSpeak(Animal & animal)
{
	animal.speak();
}
//
//多态满足条件: 
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象

void test01()
{
	Cat cat;
	DoSpeak(cat);


	Dog dog;
	DoSpeak(dog);
}


int main() {

	test01();

	system("pause");

	return 0;
}

总结:

多态满足条件

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

静态绑定和动态绑定举例

静态绑定:

#include<iostream>
using namespace std;

class Point 
{
public:
	Point(double x, double y) : x(x), y(y) { }
	double area()  const { return 0.0; }
private:
	double x, y;
};
class Rectangle: public Point 
{
public:
	Rectangle(double x, double y, double w, double h);
	double area() const  { return  w * h; }
private:
	double w, h;
};

Rectangle::Rectangle(double x, double y, double w, double h) :Point(x, y), w(w), h(h) { }

void fun(const Point &s) 
{
	cout << "Area = " << s.area() << endl;
}

int main() 
{
	Rectangle rec(3.0, 5.2, 15.0, 25.0);
	fun(rec);
	return 0;
}
//运行结果:
//Area = 0

动态绑定:

#include<iostream>
using namespace std;

class Point 
{
public:
	Point(double x, double y) : x(x), y(y) { }
	virtual double area()  const { return 0.0; }
private:
	double x, y;
};

class Rectangle:public Point 
{
public:
	Rectangle(double x, double y, double w, double h);
	virtual double area() const { return  w * h; }
  private:
	double w, h;
};

//其他函数同上例
void fun(const Point &s) 
{
	cout << "Area = " << s.area() << endl;
}

int main() 
	{
	Rectangle rec(3.0, 5.2, 15.0, 25.0);
	fun(rec);
	return 0;
}
//运行结果:
//Area = 375

动态绑定内存结构分析见4.6.11:虚函数动态绑定实现原理

4.7.2 多态案例一-计算器类

案例描述:

分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

示例:

//普通实现
class Calculator {
public:
	int getResult(string oper)
	{
		if (oper == "+") {
			return m_Num1 + m_Num2;
		}
		else if (oper == "-") {
			return m_Num1 - m_Num2;
		}
		else if (oper == "*") {
			return m_Num1 * m_Num2;
		}
		//如果要提供新的运算,需要修改源码
	}
public:
	int m_Num1;
	int m_Num2;
};

void test01()
{
	//普通实现测试
	Calculator c;
	c.m_Num1 = 10;
	c.m_Num2 = 10;
	cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;

	cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;

	cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}



//多态实现
//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public :

	virtual int getResult()
	{
		return 0;
	}

	int m_Num1;
	int m_Num2;
};

//加法计算器
class AddCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 + m_Num2;
	}
};

//减法计算器
class SubCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 - m_Num2;
	}
};

//乘法计算器
class MulCalculator :public AbstractCalculator
{
public:
	int getResult()
	{
		return m_Num1 * m_Num2;
	}
};


void test02()
{
	//创建加法计算器
	AbstractCalculator *abc = new AddCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
	delete abc;  //用完了记得销毁

	//创建减法计算器
	abc = new SubCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
	delete abc;  

	//创建乘法计算器
	abc = new MulCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
	delete abc;
}

int main() {

	//test01();

	test02();

	system("pause");

	return 0;
}

总结:C++开发提倡利用多态设计程序架构,因为多态优点很多

4.7.3 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数。如果一个类中包含了纯虚函数,那么这个类就无法实例化对象,这个类通常被称为抽象类。抽象类的子类必须重写父类中的纯虚函数,否则也属于抽象类

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

虚函数:virtual 返回值类型 函数名(参数列表){ }

当类中至少有一个纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象,只能作为基类来使用
  • 子类必须重写抽象类中的纯虚函数,否则子类也属于抽象类
  • 构造函数不能是虚函数,析构函数可以是虚函数

示例:

//抽象类
class Base
{
public:
	//纯虚函数。需要有声明,可以没有实现。这点和纯虚析构函数不一样
	//类中只要有一个纯虚函数就称为抽象类
	//抽象类无法实例化对象
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func() 
	{
		cout << "Son's func调用" << endl;
	};
};

void test01()
{
	Base * base = NULL;
	//base = new Base; // 错误,抽象类无法实例化对象
	base = new Son;
	base->func();
	delete base;//记得销毁
}

int main() {

	test01();

	system("pause");

	return 0;
}
4.7.4 多态案例二-制作饮品

案例描述:

制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例:

//抽象制作饮品
class AbstractDrinking {
public:
	//烧水
	virtual void Boil() = 0;
	//冲泡
	virtual void Brew() = 0;
	//倒入杯中
	virtual void PourInCup() = 0;
	//加入辅料
	virtual void PutSomething() = 0;
	//规定流程
	void MakeDrink() {
		Boil();
		Brew();
		PourInCup();
		PutSomething();
	}
};

//制作咖啡
class Coffee : public AbstractDrinking {
public:
	//烧水
	virtual void Boil() {
		cout << "煮农夫山泉!" << endl;
	}
	//冲泡
	virtual void Brew() {
		cout << "冲泡咖啡!" << endl;
	}
	//倒入杯中
	virtual void PourInCup() {
		cout << "将咖啡倒入杯中!" << endl;
	}
	//加入辅料
	virtual void PutSomething() {
		cout << "加入牛奶!" << endl;
	}
};

//制作茶水
class Tea : public AbstractDrinking {
public:
	//烧水
	virtual void Boil() {
		cout << "煮自来水!" << endl;
	}
	//冲泡
	virtual void Brew() {
		cout << "冲泡茶叶!" << endl;
	}
	//倒入杯中
	virtual void PourInCup() {
		cout << "将茶水倒入杯中!" << endl;
	}
	//加入辅料
	virtual void PutSomething() {
		cout << "加入枸杞!" << endl;
	}
};

//业务函数
void DoWork(AbstractDrinking* drink) {
	drink->MakeDrink();
	delete drink;
}

void test01() {
	DoWork(new Coffee);
	cout << "--------------" << endl;
	DoWork(new Tea);
}


int main() {

	test01();

	system("pause");

	return 0;
}
4.7.5 虚析构和纯虚析构

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

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

为什么需要虚析构函数?

  • 可能通过基类指针删除派生类对象;

  • 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。

析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

  • 如果一个类中有纯虚析构,该类属于抽象类,无法实例化对象,只能作为基类来使用

抽象类:构造函数不能是虚函数,析构函数可以是虚函数

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

类名::~类名(){}

示例:

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

	Animal()
	{
		cout << "Animal 构造函数调用!" << endl;
	}
	virtual void Speak() = 0;

	//析构函数加上virtual关键字,变成虚析构函数
	//virtual ~Animal()
	//{
	//	cout << "Animal虚析构函数调用!" << endl;
	//}

	//纯虚析构 需要有声明,也要有实现。跟虚析构一样。为了释放 本类中的属性在堆区开辟的内存(类内声明,类外实现)
    //纯虚函数:需要有声明,不一定要有实现
	virtual ~Animal() = 0;
};

//如果子类中有指向堆区的属性,那么要利用虚析构技术,在delete的时候,调用子类的析构函数
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();
    
    //如果一个类中有纯虚析构,该类属于抽象类,无法实例化对象,只能作为基类来使用
    //Animal aaa;

	system("pause");

	return 0;
}

总结:

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

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

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

4.7.6 多态案例三-电脑组装

案例描述:

电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)

将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装三台不同的电脑进行工作

示例:

#include<iostream>
using namespace std;

//抽象CPU类
class CPU
{
public:
	//抽象的计算函数
	virtual void calculate() = 0;
};

//抽象显卡类
class VideoCard
{
public:
	//抽象的显示函数
	virtual void display() = 0;
};

//抽象内存条类
class Memory
{
public:
	//抽象的存储函数
	virtual void storage() = 0;
};

//电脑类
class Computer
{
public:
	Computer(CPU * cpu, VideoCard * vc, Memory * mem)
	{
        cout << "电脑构造调用" << endl;
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	//提供工作的函数
	void work()
	{
		//让零件工作起来,调用接口
		m_cpu->calculate();

		m_vc->display();

		m_mem->storage();
	}

	//提供析构函数 释放3个电脑零件
	~Computer()
	{
		cout << "电脑析构调用" << endl;
		//释放CPU零件
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}

		//释放显卡零件
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}

		//释放内存条零件
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}

private:

	CPU * m_cpu; //CPU的零件指针
	VideoCard * m_vc; //显卡零件指针
	Memory * m_mem; //内存条零件指针
};

//具体厂商
//Intel厂商
class IntelCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Intel的CPU开始计算了!" << endl;
	}
};

class IntelVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Intel的显卡开始显示了!" << endl;
	}
};

class IntelMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Intel的内存条开始存储了!" << endl;
	}
};

//Lenovo厂商
class LenovoCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Lenovo的CPU开始计算了!" << endl;
	}
};

class LenovoVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Lenovo的显卡开始显示了!" << endl;
	}
};

class LenovoMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Lenovo的内存条开始存储了!" << endl;
	}
};


void test01()
{
	//第一台电脑零件
	CPU * intelCpu = new IntelCPU;
	VideoCard * intelCard = new IntelVideoCard;
	Memory * intelMem = new IntelMemory;

	cout << "第一台电脑开始工作:" << endl;
	//创建第一台电脑
	Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);
	computer1->work();
	delete computer1;

	cout << "-----------------------" << endl;
	cout << "第二台电脑开始工作:" << endl;
	//第二台电脑组装
	Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);;
	computer2->work();
	delete computer2;

	cout << "-----------------------" << endl;
	cout << "第三台电脑开始工作:" << endl;
	//第三台电脑组装
	Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;
	computer3->work();
	delete computer3;

}
4.7.7 类型转换

首先必须明确的是:由于子类继承于父类,故子类继承父类的所有成员,并且子类中还有自己特有的成员。

所以在创建对象时,子类对象所分配的内存空间一定大于父类对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 父转子,向下类型转换 不安全

    由于父类分配的内存空间比子类小, 所以向下转换,子类对象指针 可能造成访问越界

  • 子转父,向上类型转换 安全

    由于子类分配的内存空间按比父类大,所以向上转换,父类对象指针 不可能造成访问越界

  • 如果发生多态,那么转换永远是安全的

    多态,父类指针或引用指向子类对象,按照子类分配内存空间。所以不管以后进行向上转型还是向下转型都不会发生访问越界

4.7.8 重载,重写和重定义
  • 重载 同一作用域下的同名函数
    • 同一个作用域
    • 参数个数不同,参数顺序,参数类型不同
    • 和函数返回值没有关系
    • const 也可以作为重载条件(do(const Teacher& t){} do(Teacher& t) )
  • 重定义(隐藏) 参见 4.6.7
    • 有继承
    • 子类(派生类)重新定义父类(基类)中的同名成员(非 virtual 函数)
    • 当子类重新定义了父类中的同名函数,子类的成员函数会 隐藏掉父类中所有重载版本的同名函数。
  • 重写(覆盖)
    • 有继承
    • 子类(派生类)重写父类(基类)的 virtual 函数
    • 函数返回值类型,函数名字,函数形参列表必须和基类中虚函数一致
class A{
public:
    //同一作用域下 func1函数重载
    void func1(){}
    void func1(int a){}
    
    void func2(){}
    
    virtual void func3(){}
};

class B{
public:
    //重定义基类中的 func2,隐藏基类中的 func2 方法
    void func2(){}
    //重写基类中的 func3函数,也可以覆盖基类 func3
    virtual void func3(){}
};

5. 输入输出流

1. IO流概念和流的分类

程序的输入指的是从输入文件将数据传送给程序,(磁盘 —》内存)

程序的输出指的是从程序将数据传送给输出文件(内存 —》磁盘)

C++输入输出包含以下三个方面的内容:

  • 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O

  • 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O

  • 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O

C++I/OC的发展**–**类型安全和可扩展性

  • 在C语言中,用prinf 和scanf 进行输入输出,往往不能保证所输入输出的数据是可靠的安全的。

    在C++的输入输出中,编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译。因此C++的I/O操作是类型安全(type safe)的。C++的I/O操作是可扩展的,不仅可以用来输入输出标准类型的数据,也可以用于用户自定义类型的数据。

  • C++通过I/O类库来实现丰富的I/O功能。这样使C++的输人输出明显地优于C语言中的prinf和scanf,但是也为之付出了代价,C++的I/O系统变得比较复杂,要掌握许多细节。

  • C++编译系统提供了用于输入输出的iostream类库。iostream这个单词是由3个部 分组成的,即i-o-stream,意为输入输出流。在iostream类库中包含许多用于输入输出的 类。常用的见表

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

见C++基础和进阶pdf

类名作用在那个头文件
ios
istream
ostream
iostream
ifstream
ofstream
fstream
istrstream
ostrstream
strstream

2. 标准IO流

2.1 标准输入流
  1. cin:默认情况下,cin以空格、制表符和换行符作为分隔符,从输入流中读取数据。当遇到这些分隔符时,它会停止读取并存储当前数据。因此,使用cin读取字符串时,只会读取到第一个空白字符之前的内容。

cin.get() 一次读取一个字符(能够读取换行符)

cin.get(两个参数) 读取字符串,可以读取空格。换行符遗留在缓冲区

char buf[1024] = {0};
cin.get(buf, 1024);
char c = cin.get(); //cin.get()能够读取字符串
if(c == '\n'){
	cout << "换行符遗留在缓冲区" << endl; 
}
cout < buf << endl;

cin.getline() 读取一行数据,可以读取空格。换行符不在缓冲区中,而是丢弃了

char buf[1024] = {0};
cin.getline (buf, 1024);
char c = cin.get(); //cin.get()能够读取字符串
if(c == '\n'){
	cout << "换行符遗留在缓冲区" << endl; 
}
cout < buf << endl;

getline(cin, buf); //从标准输入中读取字符串并赋值给buf

cin.ignore() 忽略。默认忽略 1 个字符,如果填入参数 X,代表忽略 X个字符

cin.ignore(); //cin.ignore(2);
char c = cin.get();
cout << c << endl;

cin.peek() 偷窥

char c = cin.peek(); //输入as
cout << "c = " << endl;

c = cin.get();
cout << "c = " << endl;

c = cin.get();
cout << "c = " << endl;

cin.putback() 放回,放回原位置

char c = cin.get(); //输入hello world
cin.putback();

char buf[1024] = {0};
cin.getline(buf, 1024);
cout << buf << endl;
案例(p9 2.44.43)

案例1:判断用户输入的是数字还是字符串

案例2:用户输入0 ~ 10,输入有误,重新输入

标志位 cin.fail() 0 代表正常,1代表异常

当用户输入a,标志位变为 1

2.2 标准输出流

cout.put() 向缓冲区中写字符

cout.write() 从buffer中写num个字节到当前输出流中

cout.put('h');
cout.put('h').put('a');

char buf[1024] = "hello world";
cout.write(buf, strlen(buf));

cout << "hello world";
格式化输出
  1. 通过流成员函数格式化输出

    int main(){
    	int num = 99;
    	cout.width(20);//设置宽度
    	//cout.fill('*');//设置填充
    	//cout.setf(ios::left);	//左对齐
    	//cout.unsetf(ios::dec);//卸载十进制
    	//cout.setf(ios::hex);	//安装十六进制
    	//cout.setf(ios::showbase); //显示基数
    	//cout.unsetf(ios::hex);	//卸载十六进制
    	//cout.setf(ios::oct);	//安装八进制
    	//cout << num << endl;
    }
    
  2. 使用控制符 引入头文件<iomanip>

#include <iomanip> //控制符格式化输出 头文件

int main{
	int num = 99;
	cout << setw(20)	//设置宽度
		 //<< setfill('*')	//设置填充
		 //<< setiosflags(ios::showbase)	//显示基数
		 //<< setiosflags(ios::left)	//设置左对齐
		 //<< hex	//显示 十六进制
		 << num
		 << endl;
}

3. 文件IO

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

通过文件可以将数据持久化

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

磁盘文件类型分为两种:

  1. 文本文件 基于字符编码的文件 - 文件以文本的ASCII码形式存储在计算机中
  2. 二进制文件 -基于值编码的文件 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:

  1. ofstream:写操作
  2. ifstream: 读操作
  3. fstream : 读写操作
3.1文本文件
3.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 <fstream>

void test01()
{
    //写文件 
	ofstream ofs;
	ofs.open("test.txt", ios::out | ios::trunc);
    
    if(!ofs.isopen()){
        cout << "文件打开失败" << endl;
        return;
    }

	ofs << "姓名:张三" << endl;
	ofs << "性别:男" << endl;
	ofs << "年龄:18" << endl;

    //关闭文件
	ofs.close();
}

int main() {

	test01();

	system("pause");

	return 0;
}

总结:

  • 文件操作必须包含头文件 fstream
  • 写文件可以利用 ofstream ,或者fstream类
  • 打开文件时候需要指定操作文件的路径,以及打开方式
  • 利用<<可以向文件中写数据
  • 操作完毕,要关闭文件
3.1.2读文件

读文件与写文件步骤相似,但是读取方式相对于比较多

读文件步骤如下:

  1. 包含头文件

    #include <fstream>

  2. 创建流对象

    ifstream ifs;

  3. 打开文件并判断文件是否打开成功(ifs.isopen())

    ifs.open(“文件路径”,打开方式);

  4. 读数据

    四种方式读取

  5. 关闭文件

    ifs.close();

示例:

#include <fstream>
#include <string>
void test01()
{
	ifstream ifs;
	ifs.open("test.txt", ios::in);

	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
		return;
	}

	//第一种方式
	//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;
	}

	ifs.close();


}

int main() {

	test01();

	system("pause");

	return 0;
}

总结:

  • 读文件可以利用 ifstream ,或者fstream类
  • 利用is_open函数可以判断文件是否打开成功
  • close 关闭文件
3.2 二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为 ios::binary

3.2.1 写文件

二进制方式写文件主要利用流对象调用成员函数write

函数原型 :ostream& write(const char * buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

示例:

#include <fstream>
#include <string>

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

//二进制文件  写文件
void test01()
{
	//1、包含头文件

	//2、创建输出流对象
	ofstream ofs("person.txt", ios::out | ios::binary);
	
	//3、打开文件
	//ofs.open("person.txt", ios::out | ios::binary);

	Person p = {"张三"  , 18};

	//4、写文件
	ofs.write((const char *)&p, sizeof(p));

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

int main() {

	test01();

	system("pause");

	return 0;
}

总结:

  • 文件输出流对象 可以通过write函数,以二进制方式写数据
3.2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:istream& read(char *buffer,int len);

参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

示例:

#include <fstream>
#include <string>

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

void test01()
{
	ifstream ifs("person.txt", ios::in | ios::binary);
	if (!ifs.is_open())
	{
		cout << "文件打开失败" << endl;
	}

	Person p;
	ifs.read((char *)&p, sizeof(p));

	cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}

int main() {

	test01();

	system("pause");

	return 0;
}
  • 文件输入流对象 可以通过read函数,以二进制方式读数据

6. 类型转换

1. 静态转换(static_cast)

  • 用于类层次结构中基类(父类)和派生类(子类)之间 指针或引用的转换
    • 进行上行转换(把派生类指针或引用转换成基类表示,子转父)是安全的
    • 进行下行转换(把基类指针或引用转换成派生类表示,父转子)时,由于没有动态类型检查,所以是不安全的
  • 用于基础数据类型之间的转换,如把int 转换成 char。这种转换的安全性也要开发人员来保证
    • static_cast:只要不包含底层const,都可以使用
//1. 静态类型转换 static_cast
void test1(){
    //对于内置数据类型转换
    //语法:static_cast<目标数据类型>(原变量/对象)
    char a = 'a';
    int b = static_cast<int>(a);
    cout << b << endl;
}


class Animals{};
class Dogs : public Animals{};
class Others{};

//2. 继承中指针相互转换
void test2(){
    Animals * animals = NULL;
    Dogs * dogs = NULL;
    //子转父 安全
    Animals * animals02 = static_cast<Animals *>(dogs);
    //父转子 不安全
    Dogs * dog02 = static_cast<Dogs *>(animals);
    //无效转换
    //Others * other = static_cast<Others *>(ani);
}

//3. 继承中引用相互转换
void test3(){
    Animals animals;
    Dogs dogs;

    Animals& ani = animals;
    Dogs& dog = dogs;

    //子转父 安全
    Animals& ani2 = static_cast<Animals &>(dog);
    //父转子 不安全
    Dogs& dog2 = static_cast<Dogs &>(ani);
}

2. 动态转换(dynamic_cast)

  • dynamic_cast 主要用于类层次间的上行转换和下行转换,不支持内置数据类型转换
  • 在类层级间进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的
  • 在进行下行转换时,dynamic_cast 具有类型检查功能,比static_cast更安全
    • 父转子,失败
    • 子转父,成功
  • 如果发生多态,转换(向上或者向下)总是安全的
void test(){
    //不允许内置类型之间的转换
    char c = 'c';
    //int d = dynamic_cast<int>(c);
}

class Base{};
class Son :public Base{};
class Other{};

void test02(){
    Base * base = NUL;
    Son * son = NULL;
    
    //将 base转为 son 父转子 不安全,失败(在多态时成功)
    Son * son2 = dynamic_cast<Son *>(base);
    
    //将 son 转为 base 子转父 安全
    Base * base2 = dynamic_cast<Base *>(son);
    
    //base 转 other
    //Other * other = dynamic_cast<Other *>(base); //无法转换
}

多态时:

class Base{virtual void fun(){}};
class Son :public Base{virtual void fun(){}};
class Other{};

void test02(){
    Base * base =  new Son;
    Son * son = NULL;
    
    //发生多态,转换安全
    Son * son2 = dynamic_cast<Son *>(base);
    
    //将 son 转为 base 子转父 安全
    Base * base2 = dynamic_cast<Base *>(son);
    
    //base 转 other
    //Other * other = dynamic_cast<Other *>(base); //无法转换
}

3. 常量转换(const_cast)

该运算符用来修改类型的 const或 volatile 属性

  • 从const 转换为 非const 或者 从 volatile 转换为 非volatile

  • 常量指针被转换成非常量指针,并且仍然指向原来的对象

  • 常量引用被转换成非常量引用,并且仍然指向原来的对象

**注意:**不能直接对非指针和非引用的变量使用 const_cast 操作符去直接移除它的const

//常量指针转换成非常量指针
void test01(){
    const int *p = NULL;
    int *np = const_cast<int *>(p);

    int *pp = NULL;
    const int *npp = const_cast<const int *>(pp);

    const int a = 10; //不能对非指针或非引用进行转换
    //int b = const_cast<int>(a);
}

//常量引用转换成非常量引用
void test02(){
    int num = 10;
    int &refNum = num;

    const int &refNum2 = const_cast<const int&>(refNum);
}

4. 重新解释转换(reinterpret_cast)

interpret 是解释的意思,reinterpret 即为重新解释,此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。

这是最不安全的一种转换机制,最有可能出问题

主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针

void test(){
    int a = 10;
    int * p = reinterpret_cast<int *>(a);
}

7. 异常处理(了解即可)

7.1 异常基本概念

异常基本思想:

  • 让一个函数在发现自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或间接)调用者能够处理这个问题。即:将问题检测和问题处理相分离

7.2 异常语法

7.2.1 异常的基础语法

异常处理的关键字:

  • try throw catch

可能出现异常的代码放到try块中,利用throw抛出异常,catch捕获异常。catch(类型) 如果想捕获其他类型 catch(…)。如果捕获到的异常不想处理,继续向上抛出,利用 throw。

异常必须有函数进行处理。如果都不去处理,程序自动调用 terminate 函数,中断掉

可以抛出自定义类型异常

//自定义异常
class MyException{
public:
    void printError(){
        cout << "自定义异常" << endl;
    }
};


int myDevision(int a, int b){
    if(b == 0){
        //return -1;
        //throw -1; //这里并不是像c语言那样返回 -1 这个值,而是返回 int 类型的异常
        throw 'a';
        //throw 3.14;
        //throw MyException(); // 抛出 MyException 的匿名对象
    }
    return a/b;
 }
void test01(){
    int a = 10;
    int b = 0; //c语言中,如果b = -10 会抛出异常,与事实不符

    //c语言处理异常有缺陷,返回值不统一,返回值只有一个,无法区分是结果还是异常
    // int ret = myDevision(a, b);
    // if(ret == -1){
    //     cout << "异常" << endl;
    // }

    try{
        myDevision(a, b);
    }catch(int){//接收int类型的异常
        cout << "int类型的异常被捕获" << endl;
    }catch(char){
        //捕获到了异常,但是不想处理,继续向上抛出这个异常
        //异常必须要有函数进行处理,如果没有任何处理,程序自动调用 terminate 函数,让程序中断
        //throw;
        cout << "char类型的异常被捕获" << endl;
    }catch(MyException e){
        cout << e.printError() << endl;
    }catch(...){ //接收其他类型的异常
        cout << "其他类型的异常被捕获" << endl;
    }
 }

int main(){
    try{
    test01();
    }catch(char){
        cout << "main函数中:char类型的异常被捕获" << endl;
    }catch(...){
        cout << "main函数中:其他类型的异常被捕获" << endl;
    }

    system("pause");
    return 0;
}

总结:

  1. 若有异常则通过throw操作创建一个异常对象并抛出。
  2. 将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段
  3. 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
  4. catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
  5. 如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
  6. 处理不了的异常,可以在catch的最后一个分支,使用throw语法,向上扔
7.2.2 栈解旋(unwinding)

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋(unwinding)

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

int myDevision(int a, int b){
    if(b == 0){

        //栈解旋
        //从 try代码块开始,到 throw 抛出异常前,所有栈上的数据都会被释放掉,释放的顺序和创建的顺序相反
        Person p1;
        Person p2;

        throw -1;
    }
    return a/b;
}

void test01(){
    int a = 10;
    int b = 0;

    try{
        myDevision(a, b);
    }catch(int){//接收int类型的异常
        cout << "int类型的异常被捕获" << endl;
    }catch(...){ //接收其他类型的异常
        cout << "其他类型的异常被捕获" << endl;
    }
}

int main(){
   test01();

    system("pause");
    return 0;
}
7.2.3 异常接口声明
  • 在函数中,如果为了限定抛出异常的类型,也为了加强程序的可读性,可以用异常的接口声明

  • 语法:void func() throw(int , double) //只允许抛出 int double类型异常

  • void func() throw() 代表 不允许抛出异常

  • 如果没有包含异常接口声明,则可以抛出任何类型的异常:void func()

  • 如果一个函数抛出了它的异常接口声明中所不允许的异常,unexcepted函数会被调用,该函数默认行为调用 terminate 函数中断程序

//可以抛出所有类型异常
void func1(){
    throw 10;
}

//只能抛出 int double类型异常
void test2() throw(int, double){
    throw 3.14
}

//不能抛出任何类型异常
void test3 throw(){
    //throw 10;
}

int main(){
    try{
        //func1();
        //test2();
        //test3();
    }catch(...){
        cout << "捕获异常!" << endl; 
    }
    
    system("pause");
    return 0;
}
7.2.4 异常变量的生命周期
  • 抛出的是 throw myException(); catch(myException e) 调用拷贝构造函数,效率低

  • 抛出的是 throw myException(); catch(myException& e) 调用默认构造函数,效率高。推荐

  • 抛出的是 throw &myException(); catch(myException* e) 对象会提前释放掉,不能再非法操作

  • 抛出的是 throw new myException(); catch(myException* e) delete e;只调用默认构造函数,自己要管理释放

class myException{
public:
    myException(){
        cout << "myException默认构造函数被调用" << endl;
    }
    ~myException(){
        cout << "myException析构函数被调用" << endl;
    }
    myException(const myException& e){
        cout << "myException拷贝构造函数被调用" << endl;
    }
};

void doWork(){
    throw myException(); //抛出匿名对象
}

void test(){
    try{
        doWork();
        // 抛出的是 throw myException(); catch(myException e) 调用拷贝构造函数,效率低
        // 抛出的是 throw myException(); catch(myException& e) 调用默认构造函数,效率高。推荐
        // 抛出的是 throw &myException(); catch(myException* e) 对象会提前释放掉,不能再非法操作
        // 抛出的是 throw new myException();  catch(myException* e) delete e;只调用默认构造函数,自己要管理释放
    }catch(myException e){
        cout << "自定义异常被捕获" << endl;
    }
}
7.2.5 异常中多态使用

异常基类提供虚函数,子类对象重写虚函数,基类对象引用或指针指向子类对象,实现多态

//异常的基类
class BaseException{
public:
    virtual void printError() = 0;
};

//空指针异常
class NullPointerException : public BaseException{
public:
    void printError(){
        cout << "空指针异常" << endl;
    }
};

//数组越界异常
class OutOfRangeException : public BaseException{
public:
    void printError(){
        cout << "数组越界异常" << endl;
    }
};

void doWork(){
    // throw NullPointerException();
    throw OutOfRangeException();
}

int main(){
    try{
        doWork();
    }catch(BaseException& e){
        e.printError();
    }

    system("pause");
    return 0;
}

7.3 C++标准异常库

7.3.1 标准库介绍

标准异常类的成员:

① 在上述继承体系中,每个类都有提供了构造函数、拷贝构造函数、和赋值操作符重载。

② logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述;

③ 所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。

**标准异常类的具体描述:**见配套的pdf

异常名称描述
exception所有标准异常类的父类
bad_alloc当 operator new and operator new[],请求分配内存失败时
bad_exception
bad_typeid
bad_cast
ios_base::failure
logic_error
runtime_error
length_error
domain_error
out_of_range
invalid_argument
range_error
overflow_error
underflow_error

案例一:使用标准库提供的out_of_range,需要包含头文件 <stdexcept >

#include<stdexcept>
class Person{
public:
    Person(int age){
        if(age < 0 || age > 150){
            throw out_of_range("age must be between 0 and 150");
        }else{
            this->age = age;
        }
    }
    int age;
};

int main(){
    try{
        Person p(151);
    }
    //catch(out_of_range& e){
    catch(exception& e){ //利用多态
        cout << e.what() << endl;
    }

    return 0;
}
7.3.2 编写自己的异常类
  • 继承 exception 类,重写 what 方法
  • const char *可以隐式转换为 string
  • string 转 const char * :.c_str()
//右键:转到声明,查看 exception类中的虚函数,重写该类中的虚函数
class myOutofException : public exception{
public:
    myOutofException(const char * str){
        //const char * 可以隐式转换为 string。反之不可以
        this->errorinfo = str;
    }
    myOutofException(string str){
        this->errorinfo = str;
    }

    //重写 what()函数,打印异常信息
    //在vscode下,由于exception类中 虚函数what 有  _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT 所以,在子类中也必须加上这一行,至于VS中是如何,需要在VS中查看 exception是如何定义的
     virtual const char* what() const _GLIBCXX_TXN_SAFE_DYN _GLIBCXX_USE_NOEXCEPT{
         //将 str 转为 const char *
            return errorinfo.c_str();
    }
    

    string  errorinfo; //保存异常信息
};

class Person{
public:
    Person(int age){
        if(age < 0 || age > 150){
            //这里的 "年龄必须在 0 ~ 150 之间" 是  const char* 
            //如果写成 string("年龄必须在 0 ~ 150 之间"); 就是创建了 string的一个匿名对象
            throw myOutofException("年龄必须在 0 ~ 150 之间");
        }else{
            this->age = age;
        }
    }
    int age; 
};

int main(){
    try{
        Person p(1000);
    }
    //catch(myOutofException& e)
    catch(exception& e)  //重载
    {
        cout << e.what() << endl;
    }

    system("pause");
    return 0;
}

对c语言的一些补充

官方详细笔记为 c语言基础pdf【F:\学习资料\c++全套教程\1说明\官方笔记汇总\c笔记汇总】

微软不建议再使用 C 的传统库函数 scanf,strcpy,sprintf等,所以直接使用这些库函数会提示 C4996 错误

要想继续使用此函数,需要在源文件中添加以下指令就可以避免这个错误提

示:

#define _CRT_SECURE_NO_WARNINGS //这个宏定义最好要放到.c 文件的第一行
#pragma warning(disable:4996) //或者使用这个

1. 内存管理

1.作用域

C 语言变量的作用域分为:

  • 代码块作用域(代码块是{}之间的一段代码)
  • 函数作用域
  • 文件作用域
1.1 局部变量

局部变量也叫 auto 自动变量(auto 可写可不写),一般情况下代码块{}内部定义的变量都是自动变量,它有如下特点:

  • 在一个函数内定义,只在函数范围内有效 在所包裹的{ } 内有效

  • 在复合语句中定义,只在复合语句中有效。如if语句中定义的局部变量只在if语句包裹的{}中有效

  • 随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束

  • 如果没有赋初值,内容为随机

  • 局部变量未初始化,vs不可用

    #include <stdio.h>
    void test()
    {
    	//auto 写不写是一样的
    	//auto 只能出现在{}内部
    	auto int b = 10;
    }
    int main(void)
    {
    	//b = 100; //err, 在 main 作用域中没有 b
        if (1)
        {
    	//在复合语句中定义,只在复合语句中有效
    	int a = 10;
    	printf("a = %d\n", a);
    	}
    	//a = 10; //err 离开 if()的复合语句,a 已经不存在
    	return 0;
    }
    
1.2 静态(static)局部变量
  • 在局部变量定义前添加 static 关键字

    • static 局部变量的作用域也是在定义的函数或语句块 内有效
  • static 局部变量的生命周期和程序运行周期一样,同时 staitc 局部变量的值只初始化一次,但可以赋值多次

  • static 局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值 0,字符型变量赋空字符

  • 静态局部变量只定义一次,在全局位置。通常用来做计数器

    #include <stdio.h>
    void fun1()
    {
    	int i = 0;
    	i++;
    	printf("i = %d\n", i);
    }
    void fun2()
    {
    	//静态局部变量,没有赋值,系统赋值为 0,而且只会初始化一次
    	static int a;
    	a++;
    	printf("a = %d\n", a);
    }
    int main(void)
    {
    	fun1();
    	fun1();
    	fun2();
    	fun2();
    	return 0;
    }
    
1.3 静态(static)全局变量
  • 在全局变量定义前加 static 关键字

  • 在函数外定义,作用范围被限制在所定义的文件中。不允许通过声明(extern)导出到其他文件

  • 不同文件静态全局变量可以重名,但作用域不冲突

  • static 全局变量的生命周期和程序运行周期一样,同时 staitc 全局变量的值只初始化一次

1.4 全局变量
  • 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用 extern 声明该变量(extern 变量类型 变量名)
  • 全局变量的生命周期和程序运行周期一样
  • 不同文件的全局变量不可重名
  • 为初始化的全局变量自动初始化为 0

c语言中 全局变量 前都隐藏的加了 extern关键字,属于外部链接属性。即 c语言中下面两种定义方式是等价的

int n = 10;
extern int n = 10 //如果不初始化,就变成了全局变量的声明了
1.5 extern 全局变量声明

extern int a;声明一个变量,这个全局变量在别的源文件中已经定义了,这里只是声明,而不是定义。

extern void fun(); 声明一个函数,这个函数在别的源文件中定义了,这里只是声明。

静态全局变量不能用 extern 声明,因为静态全局变量的作用域在定义了它 的源程序文件中,不能被其他源程序文件访问

变量声明和定义的区别:

  • 变量定义需要为变量分配相应的存储空间,可以为变量指定初始值

  • 变量声明并不会为变量分配新的存储空间,只是对前面已经定义的变量进行声明,变量声明不能同时初始化

    extern int a; //声明一个全局变量a,标识着将使用其他源文件中的a变量
    extern int a = 0; //定义一个全局变量a,标识着该变量将会被其他源文件使用
    
    int a;  //定义一个全局变量a
    int a = 0; //定义一个全局变量并初始化
    
  • 变量定义只能出现一次,变量声明能出现多次

用extern声明变量的作用主要有:

  • 在函数中提前使用全局变量(提前声明)
  • 使用其他源文件中的全局变量或函数
void fun(){
    extern int n;   //提前声明全局变量n,可以把变量类型去掉,如:extern n;
    ++n;
    printf("n = %d\n", n);
}
int n = 10;

int main(){
    fun();
}
1.6 全局函数和静态函数
  • 在 C 语言中函数默认都是全局的,使用关键字 static 可以将函数声明为静态,函数定义为 static 就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用(函数声明:写出函数原型。变量声明:extern 变量类型 变量名)。

  • 对于不同文件中的 staitc 函数名字可以相同

注意:

  • 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰。
  • 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。
  • 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc 函数,那么作用域是文件级的,所以不同的文件 static 函数名是可以相同的
1.7 总结
类型作用域生命周期
auto 变量 (局部变量)一对{}内当前函数
static 局部变量一对{}内整个程序运行期
extern 变量整个程序整个程序运行期
static 全局变量当前文件整个程序运行期
extern 函数整个程序整个程序运行期
static 函数当前文件整个程序运行期
register 变量一对{}内当前函数
全局变量整个程序整个程序运行期

作用域:整个程序 > 当前文件

2. 内存布局

1. 内存布局
1. 程序运行前

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好 3 段信息,分别为:代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把 data 和 bss 合起来叫做静态区或全局区)。

代码区

  • 存放 CPU 执行的二进制机器指令。
  • 通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。
  • 代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。
  • 另外,代码区还规划了局部变量的相关信息

全局初始化数据区/静态数据区(data 段)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量,const 修饰的变量,字面值常量(100,12.3 ‘a’)等等)。

全局未初始化数据区(又叫 bss 区)

存入的是全局未初始化变量,未初始化静态变量和初始化为0的全局变量和静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

2. 程序运行后

程序在加载到内存前,代码区和全局区(data 和 bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码区(text segment)

加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的

这部分区域的大小在程序执行前就已经确定,并且该内存区域属于只读

未初始化数据区(BSS)

加载的是可执行文件 BSS 段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(未初始化的全局变量,未初始化的静态变量,初始化为0的全局变量和静态变量)的生存周期为整个程序运行过程,且被默认初始化为 0 或 NULL。

全局初始化数据区/静态数据区(data segment)

加载的是可执行文件数据段,存储于数据段(已初始化的全局变量、静态初始化数据,文字常量(只读 const, 字符串常量))的数据的生存周期为整个程序运行过程。

栈区(stack)

  • 栈是一种先进后出的内存结构,由编译器自动分配释放数据
  • 主要存放函数的形参、返回值、局部变量(不包括静态变量)函数的返回地址等
  • 在程序运行过程中实时加载和释放,因此,局部变量生存周期为申请到释放该段栈空间。即函数运行结束,相应的栈变量会自动被释放(不要返回局部变量的地址 见第四大节)
  • 栈空间比较小,不适合将大量数据存放在栈中
  • 栈空间向低地址方向生长(栈底高地址,栈顶低地址)

堆区(heap)

  • 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配, new,malloc 所分配的空间在堆区
  • 堆在内存中位于 BSS 区和栈区之间。一般由程序员手动分配和释放,若程序员不释放,程序结束时由操作系统回收。
  • 堆区由开发人员手动分配和释放,在释放之前,该块堆空间可一直使用(跨函数使用堆区空间,一个函数用于开辟堆区空间,返回值为指向该空间的指针,另一个函数接收该指针,对该堆区空间进行操作。见3.5)
  • 堆空间是由 malloc 或new 分配的空间,一般速度比较慢。而且对堆空间频繁的malloc/free会造成堆空间的不连续,容易产生内存碎片,使程序效率低下。
  • 堆空间向高地址方向生长
3. 内存四区模型
  • 代码段:.text 段。程序源代码(二进制形式)
    • 存放二进制代码(只读)
  • 数据段:只读数据段 .rodata段。包括:初始化数据段 .data段。未初始化数据段 .bss段
    • .data段:存放 已初始化的全局变量、静态变量 和 常量(data区的数据在程序执行结束时自动释放)
    • .bss段:存放 未初始化的全局变量、静态变量。存放在.bss段的变量会被默认初始化为 0或NULL
  • stack:栈。在其之上开辟栈帧
    • 存放局部变量(不包括静态变量),函数形参,返回值,函数返回地址
  • heap:堆。用户自定义数据提供空间
    • 存放被动态分配的内存段(new、malloc)
    • 从堆中分配的内存仅能通过指针访问
4. 变量静态分配和动态分配的区别

静态分配:

  • 程序编译期间分配空间,并且 大小是固定的
  • 由系统自动释放(回收)分配的空间
  • 静态分配的变量范围速度快,存放在 数据段 或 栈空间内

动态分配:

  • 程序执行期间分配空间,并且 大小在执行时可以改变
  • 由程序员手动释放 分配的空间
  • 动态分配的变量访问速度慢,存放在堆区
int a[10];
int * a = (int *)malloc(10 * sizeof(int));
2. 存储类型总结

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

#include <stdio.h>
#include <stdlib.h>
int e;
static int f;
int g = 10;
static int h = 10;
int main()
{
	int a;
	int b = 10;
	static int c;
	static int d = 10;
	char *i = "test";
	char *k = NULL;
	printf("&a\t %p\t //局部未初始化变量\n", &a);
	printf("&b\t %p\t //局部初始化变量\n", &b);
    printf("&c\t %p\t //静态局部未初始化变量\n", &c);
	printf("&d\t %p\t //静态局部初始化变量\n", &d);
	printf("&e\t %p\t //全局未初始化变量\n", &e);
	printf("&f\t %p\t //全局静态未初始化变量\n", &f);
	printf("&g\t %p\t //全局初始化变量\n", &g);
	printf("&h\t %p\t //全局静态初始化变量\n", &h);
	printf("i\t %p\t //只读数据(文字常量区)\n", i);
	k = (char *)malloc(10);
	printf("k\t %p\t //动态分配的内存\n", k);
	return 0;
}
3 .内存操作函数
1) memset()
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将 s 的内存区域的前 n 个字节以参数 c 填入
参数:
s:需要操作内存 s 的首地址
c:填充的字符,c 虽然参数为 int,但必须是 unsigned char , 范围为 0~255
n:指定需要设置的大小
返回值:s 的首地址
int a[10];
memset(a, 0, sizeof(a));
memset(a, 97, sizeof(a));
int i = 0;
for (i = 0; i < 10; i++)
{
	printf("%c\n", a[i]);
}

主要用途:给内存清空 memset(a, 0, sizeof(a));

一个小问题:这些函数是字节操作函数

memset(a, 2, 5 *sizeof(short)); //会将a中所以元素设置为514.因为每个字节都设置为2,00000010 00000010,对应10进制514
2) memcpy()
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
功能:拷贝 src 所指的内存内容的前 n 个字节到 dest 所值的内存地址上。
参数:
dest:目的内存首地址
src:源内存首地址,注意:dest 和 src 所指的内存空间不可重叠,可能会导致程序报错
n:需要拷贝的字节数
返回值:dest 的首地址

用途1:给数组进行赋值

int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10];

memcpy(b, a, sizeof(a));

//打印 b中内容
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d, ", b[i]);
}
printf("\n");
//memcpy(&a[3], a, 5 * sizeof(int)); //err, 内存重叠

strcpy() 遇到 \0 结束拷贝,但memcpy 不会

void print(char *arr, int len)
{
	for(int i = 0; i < len; i++)
	{
		printf("%c", arr[i]);
	}
	printf("###\n");
}

int main()
{
	char dst[64] = {0};
	char src[64] = "hello\0world";
	
	strcpy(dst, src, sizeof(src));
	print(dst, sizeof(src));
	
	memset(dst, 0, sizeof(dst));
	mencpy(dst, src, sizeof(src));
	print(dst, sizeof(dst));

	return 0;
}
3) memmove()

memmove()功能用法和 memcpy()一样,区别在于:dest 和 src 所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比 memcpy()低些。

memmove 内存移动

void test()
{
	int arr[5] = {10, 20, 30, 40, 50};
	//通过内存拷贝,实现内存操作
    //不推荐,有可能和预期效果不同
    //从前往后拷贝  10 20 20 20 20
    //从后往前拷贝  10 20 20 30 40
//    memcpy(arr+2, arr+1, 3 * sizeof(int)); 
    
    //该函数的操作是:将 待移动的数据{20 30 40}拷贝一份,然后存入到 {30, 40, 50}中对应的位置,所以效率比memcpy()慢一些 
    mommove(arr+2, arr+1, 3 * sizeof(int));
    
    for(int i = 0; i < 5; i++)
    {
        printf("%d\n", arr[i]);
    }
}
4) memcmp()
#include <string.h>
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较 s1 和 s2 所指向内存区域的前 n 个字节
参数:
s1:内存首地址 1
s2:内存首地址 2	
n:需比较的前 n 个字节
返回值:
相等:=0
大于:>0
小于:<0
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int flag = memcmp(a, b, sizeof(a));
printf("flag = %d\n", flag);
void test()
{
	char str1[32] = "hello\0world";
	char str2[32] = "hello\0aaa";
	
	if(strcmp(str1, str2) == 0)
	{
		printf("strcmp 对比结果:str1 == str2\n");
	}
	else
	{
		printf("strcmp 对比结果:str1 != str2\n");
	}
	
	if(memcmp(str1, str2) == 0)
	{
		printf("memcmp 对比结果:str1 == str2\n");
	}
	else
	{
		printf("memcmp 对比结果:str1 != str2\n");
	}
}
5) memchr()
#include <string.h>
int memcmp(const void *s1, char c, size_t count);
功能:
    在s1开头的count个字节中查找字符c第一次出现的位置
4. 堆区内存分配和释放

size_t 类型 是 unsigned int类型的别名

1) malloc()

malloc申请后空间的值是随机的,并没有进行初始化。一般用 memset进行初始化

#include <stdlib.h>
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为 size 字节的连续区域,用来存放类型说
明符指定的类型。分配的内存空间内容不确定,一般使用 memset 初始化。
参数:
size:需要分配内存大小(单位:字节)
返回值:
成功:分配空间的起始地址
失败:NULL

空间连续时,当成数组使用

free 后的空间,不会立即失效。通常将 free 后的地址 置为 NULL

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
	int count, *array, n;
	printf("请输入要申请数组的个数:\n");
	scanf("%d", &n);
    
	array = (int *)malloc(n * sizeof (int));
    
	if (array == NULL)
	{
	printf("申请空间失败!\n");
	return -1;
	}
    
	//将申请到空间清 0
	memset(array, 0, sizeof(int)*n);
    
	for (count = 0; count < n; count++) /*给数组赋值*/
		array[count] = count;
    
	for (count = 0; count < n; count++) /*打印数组元素*/
		printf("%2d", array[count]);
    
	free(array); //释放申请的内存	
    //free 后的空间,不会立即失效。这个for循环是可以执行的,array 为野指针。为了避免这种情况发生,通常将free后的地址置空
    
    for(int i = 0; i < n; i++)
    {
        printf("%2d", *(array + i)); 
    }
    
    array = NULL;
    
    
	return 0;
}
2) free()
#include <stdlib.h>
void free(void *ptr);
功能:释放 ptr 所指向的一块内存空间,ptr 是一个任意类型的指针变量,指向被释放区域的
首地址。对同一内存空间多次释放会出错。
参数:
ptr:需要释放空间的首地址,被释放区应是由 malloc 函数所分配的区域。
返回值:无

free 操作的地址必须是 malloc 申请的 地址。否则出错

如果malloc之后的地址一定会变化,那么使用临时变量tmp 保存malloc分配的地址

int main()
{
	int *p = (int *)malloc(sizeof(int) * 10);
	if(p == NULL)
	{
   		printf("malloc error\n");
    	return -1;
	}

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

    /*
	for(int i = 0; i < 10; i++)
	{
   		printf("%d ", *(p +i));
  	}
	p++;
    
	free(p); //free 操作的地址必须是 malloc 申请的 地址。否则出错.进行了 p++操作。free操作的不是malloc申请的地址
	p = NULL;
	
	*/
    
    
    char * tmp = p; //记录malloc返回的地址值。用于free
    for(int i= 0; i < 10; i++)
    {
        printf("%d ", *p);
        p++;
    }
    free(tmp);
    tmp = NULL;

	return 0;
}
3) calloc()

在申请内存后,对空间逐一进行初始化,并设置初始值为0; (效率要低一点)

#include <stdlib.h>
void *calloc(size_t count, size_t size);
功能:为具有 count 个长度为 size 元素的数组分配内存
参数:
    count: 所需内存单元数量
	size: 每个内存单元的大小(单位:字节)
返回值:
    成功:分配空间的起始地址
    失败:NULL
void test()
{
//	int *p = (int *)malloc(sizeof(int) * 10); //malloc 不会给内存进行初始化
	
    int *p = (int *)calloc(10, sizeof(int)); //利用 calloc 分配的内存会自动初始化为0
	for(int i = 0; i < 10; i++)
	{
		printf("%d\n", p[i]);
	}
    
    //手动开辟,手动释放
    if(p != NULL)
    {
        free(p);
        p = NULL;
    }
}
4) realloc()

一般用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作)

#include<stdlib.h>	
void *realloc(viod *ptr, size_t size);
功能:重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
    realloc不会自动清理增加的内存,需要手动清理
    如果指定的地址后面有连续的空间,那么就会在已有地址基础上增加内存(空间足够大)
    如果指定的地址后面没有足够的空间进行扩容,那么realloc会重新分配新的连续内存,把旧内存的值拷贝到新内       存,同时释放旧内存
    扩展出来的新空间是没有被初始化过的
参数:
    ptr: 为之前用malloc或者calloc分配的内存地址,如果此参数等于NULL,那么和realloc与malloc功能一致
    size: 为重新分配内存的大小, 单位:字节
返回值:
    成功:新分配的堆内存地址
    失败:NULL
void test()
{
    int *p = (int *)malloc(sizeof(int) * 10);
    for(int i = 0; i < 10; i++)
    {
        p[i] = 100 +i;
    }
    
    //遍历数组
    for(int i = 0; i < 10; i++)
    {
        printf("%d\n", p[i]);
    }
    printf("realloc 打印前:%d\n", p);
    //relloc(p, sizeof(int) * 2000); 直接这样用,后续使用p相当于操作 悬空指针
    p = (int *)relloc(p, sizeof(int) * 2000);// p = relloc(sizeof(int) * 20);
    printf("realloc 打印后:%d\n", p);
    printf("----------------------\n");
     for(int i = 0; i < 20; i++)
    {
        printf("%d\n", p[i]);
    }
}
5)二级指针对应的 heap空间
  • 申请外层指针:char **p = (char **)malloc(sizeof(char *) * 5);

  • 申请内层指针:for(int i = 0; i < 5; i++)

    ​ {

    ​ p[i] = (char *)malloc(sizof(char) * 10)

    ​ }

  • 使用:for(int i = 0; i < 5; i++)

    ​ {

    ​ strcpy(p[i], “helloheap”);

    ​ }

    不能修改p的值

  • 释放:

    • 释放内层:for(int i = 0; i < 5; i++)

      ​ {

      ​ free(p[i]);

      ​ }

    • 释放外层:free§;

int main()
{
    //申请外层空间
    //int **p ==> int *p[10] 一个数组,数组元素为 int*类型
    int **p =(int **)malloc(sizeof(int *) * 3); 
    
    //申请内层空间
    for(int i = 0; i < 3; i++)
    {
        p[i] = (int *)malloc(sizeof(int) * 5); //数组中每个指针指向一个int类型数组,大小为 5
    }
    
    //写数据  ---> 当成二维数组使用
    for(int i = 0; i < 3; i++)
    {
        for(int j = 0; j < 10; j++)
        {
            p[i][j] =  i + j;
        } 
    }
    
    //读数据
     for(int i = 0; i < 3; i++)
    {
        for(int j = 0; j < 10; j++)
        {
        	printf("%d ", *(*(p + i) + j)); //p[i][j] ==> *(p + i)[j] ==> *(*(p + i) + j)
        } 
    }
    
    //释放空间
    //先释放内层空间
    for(int i = 0; i < 3; i++)
    {
        free(p[i]);
        p[i] = NULL;
    }
    //后释放外层空间
    free(p);
    p = NULL;
    
    return 0;
}

案例二:

#define _CRT_SECURE_NO_DEPRECATE  //解决 VS 中的4996 错误
#include <iostream>
#include<string>
using namespace std;

struct Teacher
{
    char* name;
    int age;
};

void test01()
{
    // 二级指针创建 老师结构体数组
    struct Teacher** teaArray = (Teacher**)malloc(sizeof(Teacher*) * 3);

    for (int i = 0; i < 3; i++)
    {
        //为每个老师结构体分配内存
        teaArray[i] = (Teacher *)malloc(sizeof(Teacher));

        //为每个老师的名字,在堆区分配内存 并赋值
        teaArray[i]->name = (char *)malloc(sizeof(char) * 64);
        sprintf(teaArray[i]->name, "Teacher_%d", i + 1);

        teaArray[i]->age = 30 + i;
    }

    //打印信息
    for (int i = 0; i < 3; i++)
    {
        printf("姓名:%s  年龄:%d\n", teaArray[i]->name, teaArray[i]->age);
    }

    //释放内存
    //先释放内层,后释放外层
    for (int i = 0; i < 3; i++)
    {
        //顺序不能颠倒
        if (teaArray[i]->name != NULL)
        {
            free(teaArray[i]->name);
            teaArray[i]->name = NULL;
        }

        if (teaArray[i] != NULL)
        {
            free(teaArray[i]);
            teaArray[i] = NULL;
        }
    }

    if (teaArray != NULL)
    {
        free(teaArray);
        teaArray = NULL;
    }
}

int main()
{
    test01();
}

3. 内存分区代码分析

1) 返回栈区地址
  • 不能返回局部变量的地址
#include <stdio.h>
int *fun()
{
	int a = 10;
	return &a;//函数调用完毕,a 释放
}
int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun(); //p 指向一块已经被释放掉了的空间
	*p = 100; //操作野指针指向的内存,err
	return 0;
}
2) 返回 data 区地址
  • 可以返回静态(static)变量的地址
#include <stdio.h>
int *fun()
{
	static int a = 10;
	return &a; //函数调用完毕,a 不释放
}
int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun();
	*p = 100; //ok
	printf("*p = %d\n", *p);
	return 0;
}
3) 值传递 1
#include <stdio.h>
#include <stdlib.h>
void fun(int *tmp)
{
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
}
int main(int argc, char *argv[])
{
	int *p = NULL;
	fun(p); //值传递,形参修改不会影响实参
	printf("*p = %d\n", *p);//err,操作空指针指向的内存
	return 0;
}
4) 值传递 2
#include <stdio.h>
#include <stdlib.h>
void fun(int *tmp)
{
	*tmp = 100;
}
int main(int argc, char *argv[])
{
	int *p = NULL;
	p = (int *)malloc(sizeof(int));
	fun(p); //值传递
	printf("*p = %d\n", *p); //ok,*p 为 100
	return 0;
}
5) 返回堆区地址
#include <stdio.h>
#include <stdlib.h>

int *fun()
{
	int *tmp = NULL;
	tmp = (int *)malloc(sizeof(int));
	*tmp = 100;
	return tmp;//返回堆区地址,函数调用完毕,不释放 malloc() 分配的堆区空间,但 tmp 变量会随着函数执行完毕而释放
}

int main(int argc, char *argv[])
{
	int *p = NULL;
	p = fun(); // p指向了堆区的那块由 tmp 指向的空间
    printf("*p = %d\n", *p);//ok
    
	//堆区空间,使用完毕,手动释放
	if (p != NULL)
	{
		free(p);
		p = NULL;
	}
	return 0;
}

4. 内存操作注意事项(对3代码的补充)

  • 不要返回局部变量的地址

  • 可以返回静态变量的地址

    int * fun1()
    {
        int num = 10; 
        return &num;
    }
    
    
    int * fun2()
    {
        static int num = 10;
        return &num;
    }
    
    void test01()
    {
        int *p = fun1();
        printf("*p = %d\n", *p); //第一次输出正确结果是编译器的功劳
        printf("*p = %d\n", *p);
        //结果不重要,因为fun1()调用后,内存被释放掉了,使用这块内存属于非法操作
    }
    
    void test02()
    {
        int *p = fun2();
        printf("*p = %d\n", *p);
        printf("*p = %d\n", *p);
        printf("*p = %d\n", *p);
        printf("*p = %d\n", *p);
    }
    
    int main()
    {
        test01();
        test02();
    }
    
  • 不要操作已经释放了的堆区空间

    void test0()
    {
        int *p = malloc(sizeof(int));
        printf("p = %d\n", p);
        *p = 100;
        
        free(p);
        printf("p = %d\n", p); // free 只是断开了 p所指向的那块空间,但 p 的值还是那块空间的地址
        
        *p = 2000; // error 因为上面已经写了 free,不可以在操作这块内存
    }
    
  • 不要释放野指针

    void tests01()
    {
        int *p = malloc(sizeof(int));
        free(p);
        free(p);//此时 p 属于 野指针,野指针不可以被释放
    }
    
    void test02()
    {
        int *p = malloc(sizeof(int));
        
        if(p != NULL)
        {
            free(p);
            p = NULL;
        }
        
        free(p); // p 现在是 空指针, 可以进行 free 操作
    }
    
同级指针修饰内存失败及其解决方法

同级指针进行的是值传递,

void allocateSpace(int *pp)  // 形参pp 存放的是 实参p 的值,即 pp = NULL;
{
    pp = malloc(sizeof(int)); // pp 指向了堆中一块内存空间
}

void test()
{
    int *p = NULL;
    
    allocateSpace(p); //这里是 值传递,不会影响实参p的值: p = NULL
    
    *p = 1000; // p == NULL
    
    printf("*p = %d\n", *p);
}

int main()
{
    test();
}

解决上述问题:

  • 利用函数返回值

    int * allocateSpace()
    {
    	int *pp = malloc(sizeof(int));
        return pp;
    }
    
    void test()
    {
        int *p = NULL;
        
        p = allocateSpace(); //将pp的返回值(指向堆区的地址)赋值给p后,allcateSpace()在栈中的空间被释放
        
        *p = 1000;
        
        printf("*p = %d\n", *p);
        
        if(p != NULL)
        {
            free(p);
            p = NULL;
        }
    }
    
    int main()
    {
        test();
    }
    
  • 利用高级指针

    void allocateSpace(int **pp)  // 形参 pp 存放的是 实参p 的地址
    {
        *pp = malloc(sizeof(int)); //*pp 代表着 p, 即为 p 在堆中分配了一块内存
    }
    
    void test()
    {
        int *p = NULL;
        
       	allocateSpace(&p); 
        
        *p = 1000;
        
        printf("*p = %d\n", *p);
        
        if(p != NULL)
        {
            free(p);
            p = NULL;
        }
    }
    
    int main()
    {
        test();
    }
    
  • 将下面的代码封装成函数的注意事项

    if(p != NULL)
    {
    free§;
    p = NULL;
    }

    void freeSpace(int *pp)
    {
    	if(pp != NULL)
    	{
    		free(pp);
    		pp = NULL;
    	}
    }
    

    因为别的函数调用该函数释放内存时,要将需要释放的指针p 传入到 函数 freeSpace中。而这时传入的参数明显是值传递,即 pp 中存放的 是 p 的值,也就是 p所指向的那块空间的地址。

    所以 free§后将 p 所指向的那块内存释放了,但是后面的 pp 操作只是将 pp 的值 置为 NULL,对实参 p 的值没有影响,所以 光调用这个函数,会使 实参 p 变为一个 野指针。

    为了避免上述情况,主调函数 在调用 freeSpace 函数后,还应该中添加 p = NULL; 使 p 的值设置为 NULL来避免 p 变为一个野指针

    void test() //主调函数
    {
        int *p = malloc(sizeof(int));
        freeSpace(p);
        p = NULL;
    }
    

5. 函数的调用模型

1. 宏函数

预处理时发生宏替换

  • 宏函数和宏常量都是用 #define定义出来的内容
  • 在项目中,经常将一些短小而又频繁使用的函数写成宏函数(有点像c++中的内联函数)
  • 这是由于宏函数没有普通函数参数压栈,跳转,返回等时间上的开销,可以提高程序的效率

注:宏函数通常需要加括号,保证运行的完整

// 加小括号是为了确保运算的优先级 (x,y可以是表达式)
#define MYADD(x, y) ((x) + (y))

void test()
{
    int a = 10;
    int b = 20;
    printf("a + b = %d\n", MYADD(a, b) * 20);//预处理时将  MYADD(a, b) 替换为 (a + b)
}

将频繁短小的函数封装为宏函数,以空间换时间

2. 函数调用流程、调用惯例

主调函数调用被调函数时,要记录被调函数返回地址,并将被调函数的形参,被调函数的局部变量,函数返回值压栈。

当函数的返回值小于4字节时,函数返回值不会入栈,而是存放到了寄存器中

  • 形参入栈的顺序是怎样的?
  • 被调函数执行完毕后,形参由主调函数释放,还是由被调函数 释放?(被调函数中定义的局部变量由被调函数自己释放)

为了解决这里的问题,有了调用惯例

调用惯例:

  • ​ 主调函数和被调函数对于函数是如何调用而遵守的约定

  • ​ c/c++中存在多个调用惯例,默认使用的调用惯例为 cdecl

    cdecl不是标准的关键字,在不同的编译器里可能有不同的写法。例如gcc中就不存在 __cdecl 这样的关键字

    调用惯例控制形参出栈参数传递名字修饰
    cdecl主调函数形参从右至左入栈下划线 + 函数名
    stdcall被调函数形参从右至左入栈下划线 + 函数名+ @ +参数字节数
    fastcall被调函数前两个参数由寄存器传递,后面从右到左@ + 函数名 + @ +参数的字节数
    pascal被调函数形参从左至右入栈较为复杂,见相关文档
3. 栈的生长方向
  • 先进后出

  • 栈顶低地址,栈底高地址 (跟堆区正好相反)

    int main()
    {	
    	int a = 123;
    	int b = 234;
    	int* pa = &a;
    	int* pb = &b;
    	printf("%d\n", pa); //相差12,编译器内部实现的,记录变量上下文信息
    	printf("%d\n", pb);
    
    	return 0;
    }
    
4. 内存的存储方式

高地址:栈底端

低地址:栈顶端

小端序:低位字节数据存放在低地址 (家用电脑)

大端序:高位字节数据存放在高地址 (服务器)

int main()
{
	int a = 0x11223344;
	
	char* p =(char*) &a;

	printf("%x\n", *p); //44
	printf("%x\n", *(p + 1)); //33
	printf("%x\n", *(p + 2)); //22
	printf("%x\n", *(p + 3)); //11
	
	return 0;
}

2. 文件管理

Linux下一切皆为文件

文件:

  • 磁盘文件:分为 二进制文件 和文本文件
  • 设备文件:如鼠标,键盘等

二进制文件和文本文件:编码方式不同(字符编码,可以用记事本直接打开,浪费空间;值编码,不能用记事本直接打开,节约空间)

1.概述

在 C 语言中用一个指针变量指向一个文件,这个指针称为文件指针

typedef struct
{
short level; //缓冲区"满"或者"空"的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer;//数据缓冲区的位置
unsigned ar; //指针,当前的指向
unsigned istemp; //临时文件,指示器
short token; //用于有效性的检查
}FILE;

FILE 是系统使用 typedef 定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息.。通过文件指针就可对它所指的文件进行各种操作

C语言中有三个特殊的文件指针(在文件描述符表中的前三个位置)由系统默认打开,用户无需定义即可直接

使用:(既然是文件指针,就可以将其用 fclose 来关闭)

  • stdin: 标准输入,默认为当前终端(键盘),我们使用的 scanf、getchar 函数默认从此终端获得数据。

  • stdout:标准输出,默认为当前终端(屏幕),我们使用的 printf、puts 函数默认输出信息到此终端

  • stderr:标准出错,默认为当前终端(屏幕),我们使用的 perror 函数默认输出信息到此终端

fclose(stdin) 进行此项操作,不能向终端输入数据

void test()
{
    printf("你好\n");
    printf("你好\n");
    printf("你好\n");
    fclose(stdout);
    printf("你好\n");
    printf("你好\n");
}
文件指针fp为空常见原因
  • ①如果是写文件时,磁盘已满则 返回 NULL

  • ②如果是读文件,文件不存在时返回空指针

    故在写文件,或者读文件时,一定要先判断 文件指针是否为空,如果为空,则判定该文件打开失败

2. 文件的打开和关闭

1.打开文件 fopen

任何文件使用之前必须打开:

#include <stdio.h>
FILE * fopen(const char * filename, const char * mode);
功能:打开文件
参数:
filename:需要打开的文件名,根据需要加上路径
mode:打开文件的模式设置
返回值:
成功:文件指针
失败:NULL

第一个参数的几种形式:

FILE *fp_passwd = NULL;
//相对路径:

//打开当前目录 passdw 文件:源文件(源程序)所在目录
FILE *fp_passwd = fopen("passwd.txt", "r");
//打开当前目录(test)下 passwd.txt 文件
fp_passwd = fopen(". / test / passwd.txt", "r");

//打开当前目录上一级目录(相对当前目录)passwd.txt 文件
fp_passwd = fopen(".. / passwd.txt", "r");
//绝对路径:
//打开 C 盘 test 目录下一个叫 passwd.txt 文件
fp_passwd = fopen("c:/test/passwd.txt","r");

第二个参数的几种形式(打开文件的方式):

打开模式含义
r 或 rb以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)
w 或 wb以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a 或 ab以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件
r+或 rb+以可读、可写的方式打开文件(不创建新文件)
w+或 wb+以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a+或 ab+以添加方式打开文件,打开文件并在末尾更改文件,若文件不存在则创建文件

注意:

  • b 是二进制模式的意思,b 只是在 Windows 有效,在 Linux 用 r 和 rb 的结果是一样的

  • Unix 和 Linux 下所有的文本文件行都是\n 结尾,而 Windows 所有的文本文件行都是\r\n 结尾

  • 在 Windows 平台下,以“文本”方式打开文件,不加 b:

    • 当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n” (换行后光标在末尾,使用 \r (回车)将光标移到首位)
    • 当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入
    • 以"二进制"方式打开文件,则读 或者 写都不会进行上述这样的转换
  • 在 Unix/Linux 平台下,“文本”与“二进制”模式没有区别,“\r\n” 作为两个字符原样输入输出

int main(void)
{
	FILE *fp = NULL;
	// 	"\\"这样的路径形式,只能在 windows 使用 ---> 添加转义字符
	// 	"/"这样的路径形式,windows 和 linux 平台下都可用,建议使用这种
	// 	路径可以是相对路径,也可是绝对路径
	fp = fopen("../test", "w");
    
	//fp = fopen("..\\test", "w");
	if (fp == NULL) //返回空,说明打开失败
	{
		//perror()是标准出错打印函数,能打印调用库函数出错原因
		perror("open");
		return -1;
	}
	return 0;
}
2.文件的关闭 fclose

任何文件在使用后应该关闭:

  • 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存
  • 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数(1024 文件描述符表最大容量),再次调用 fopen 打开文件会失败
  • 如果没有明确的调用 fclose 关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭
  • 刷新缓冲区
#include <stdio.h>
int fclose(FILE * stream);
功能:关闭先前 fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
	stream:文件指针
返回值:
	成功:0
	失败:-1
FILE * fp = NULL;
fp = fopen("abc.txt", "r");
fclose(fp);

3. 文件的顺序读写

文件结尾
  • 文本文件 EOF feof()
  • 二进制文件 feof()

在 C 语言中,EOF 表示文件结束符(end of file)。在 while 循环中以 EOF 作为文件结束标志,这种以 EOF 作为文件结束标志的文件,必须是文本文件。

在文本文件中,数据都是以字符的 ASCII 代码值的形式存放。我们知道,ASCII 代码值的范围是 0~127,不可能出现-1,因此可以用 EOF 作为文件结束标志。

#define EOF (-1)

当把数据以二进制形式存放到文件中时,就会有-1 值的出现,因此不能采用EOF 作为二进制文件的结束标志。为解决这一个问题,ANSI C 提供一个 feof函数,用来判断文件是否结束。feof 函数既可用以判断二进制文件又可以判断文本文件。

#include <stdio.h>
int feof(FILE * stream);
功能:检测是否读取到了文件结尾。判断的是最后一次“读操作的内容”,不是当前位置内
容(上一个内容)。
参数:
stream:文件指针
返回值:
非 0 值:已经到文件结尾
0:没有到文件结尾
1. 按字符读写文件 fgetc fputc
1. fgetc 按字符读取文件中内容
#include <stdio.h>
int fgetc(FILE * stream);
功能:从 stream 指定的文件中读取一个字符
参数:
	stream:文件指针
返回值:
	成功:返回读取到的字符
	失败:-1 (#define EOF -1)
char ch;
#if 0   //条件编译,如果 if 语句后面的表达式为 真,则执行 #if ~ #endif中的语句 (#if 表达式 #else  #endif  表达式为真,执行 #if ~ #else间的语句,否则,执行 #else ~ #endif 中的语句)
while ((ch = fgetc(fp)) != EOF)
{
	printf("%c", ch);
}
printf("\n");
#endif

while (!feof(fp)) //文件没有结束,则执行循环
{
	ch = fgetc(fp);
	printf("%c", ch);
}
printf("\n");

不建议使用 feof 按照字符方式读文件,原因是 feof 有滞后性,会读出 EOF

printf("%cAAA", EOF);

如果属性开辟在堆区(如:结构体嵌套一级指针),不要将指针存入文件中,要将指针指向的内容放到文件中

void test()
{
    FILE * fr = fopen("text.txt", "r");
    if(fr = NULL)
    {
        printf("文件打开失败\n");
        return;
    }
    
    char ch = 0;
   	while((ch = fgetc(fr)) != EOF) //按字符读取文件
    {
        printf("%c", ch);
    }
}
2. fputc 按字符将内容写入文件
#include <stdio.h>
int fputc(int ch, FILE * stream);
功能:将 ch 转换为 unsigned char 后写入 stream 指定的文件中
参数:
	ch:需要写入文件的字符
	stream:文件指针
返回值:
	成功:成功写入文件的字符
	失败:返回-1
void test()
{
    FILE * fp = fopen("text.txt", "w");
    if(fp == NULL)
    {
        printf("文件打开失败\n");
        return;
    }
    
    /*
    	当 fopen()第二个参数为 w,即写入文本文件时。如果buf[] 字符串中有换行 \n,则系统会将 "\n" 转换			成 "\r\n" 写入(可以通过vs用二进制方式打开文件来查看)
    	
    	当 fopen()第二个参数为 wb,即写入二进制文件时。不会出现以上操作
    */
    //缓冲区 buf,减少对磁盘的读写次数, 提高读写磁盘的效率
    char buf[32] = "hello world"; //注意传入 "hello world" 和 "hello world\n" 光标位置的区别
    
    int i = 0; 
    while(buf[i] != '\0')
    {
        fputc(buf[i], fp); //按字符将缓冲区中内容写入文件
        i++;
    }
}
3. 案例

用户从屏幕输入字符,都会直接写到文件中,直到用户输入 :q 代表输入结束

void test03()
{
	FILE* fw = fopen("text3.txt", "w");
	if (fw == NULL)
	{
		printf("打开文件失败\n");
		return;
	}

	while (1)
	{
		char buf[128] = { 0 };

		fgets(buf, sizeof(buf), stdin); // 按行从屏幕中读取

		if (strncmp(buf, ":q", 2) == 0)
		{
			return;
		}

		// buf --> helloworld\n\0  初始化时将 buf 中所有的元素都初始化为 0
		int i = 0;
		while (buf[i] != '\0')
		{
			fputc(buf[i], fw);
			i++;
		}
	}

	// 关闭文件
	fclose(fw);
}
2. 按行读写文件 fgets fputs

在基础篇数据类型一章中字符串的笔记中有所提交

1. fgets() 按行读文件
#include <stdio.h>
char * fgets(char * str, int size, FILE * stream);
功能:从 stream 指定的文件内读入字符,保存到 str 所指定的内存空间,直到出现换行字符、读到文件结尾或是已读		了 size - 1 个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
参数:
	str:字符串
	size:指定最大读取字符串的长度(size - 1)
	stream:文件指针
返回值:
	成功:成功读取的字符串
	读到文件尾或出错: NULL

注意:feof函数会带来一些问题,以及 该程序如何解决这些问题

void test()
{
    FILE * fr = fopen("text.txt", "r");
    if(fr == NULL)
    {
        printf("文件打开失败\n");
        return;
    }

#if 0
    char buf[1024] = {0};
    while(!feof(fr)) //如果没有读取到文件尾,一直按行读取
    {
        char * ret = fgets(buf, sizeof(buf), fr);
        if(ret == NULL) //如果不加这句,当 feof指向文件末尾时(\n), buf 中不会记录\n,而是保存着上一次的内容,所以会再次输出上一次的内容。feof的滞后性
        {
            break;
        }
        
        // 将 \n  改为 \0,否则会出现两个换行
        buf[strlen(buf) -1] = '\0';
        printf("%s\n", buf);
    }
    
#else 
    char buf[1024] = {0};
    while(fgets(buf, sizeof(buf), fr))
    {
        buf[strlen(buf) - 1] = '\0';
        printf("%s\n", buf);
    }
#endif
    
    fclose(fr);
}
2. fputs() 按行写入文件
#include <stdio.h>
int fputs(const char * str, FILE * stream);
功能:将 str 所指定的字符串写入到 stream 指定的文件中,字符串结束符 '\0' 不写入文件。
参数:
	str:字符串
	stream:文件指针
返回值:
	成功:0
	失败:-1
void test()
{
    // 打开文件
	FILE * fw = fopen("text.txt", "w");
	if(fw == NULL)
	{
		printf("文件打开失败\n");
		return;
	}
    
    //一定要写换行符,否则这些字符串全在一行
    //在 VS2019中,要在 char * buf[]前加 const 关键字修饰,否则会报错。在 devc++ 不用这样
	char * buf[]= {"关山难越,\n", "谁悲失路途之人?\n", "萍水相逢,\n", "尽是他乡之客。\n"};
	int len = sizeof(buf) / sizeof(buf[0]);
    
	for(int i = 0; i < len; i++)
	{
		fputs(buf[i], fw);
	}
    
    // 关闭文件
    fclose(fw);
}
3. 案例

有个文件大小不确定,每行内容都是一个四则运算表达式,还没有算出结果, 写一个程序,自动算出其结果后修改文件

头文件(test.h)

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>

//出题
void setQuestion();

//解题
void answerQuestion();

源文件(test.c)

#include "test.h"

//出题
void setQuestion()
{
	//设置随机数种子
	srand((size_t)time(NULL));

	//创建随机数和运算符
	int num1 = 0;
	int num2 = 0;
	char ch = 0;
	char oper[] = { '+', '-', '*', '/' };

	FILE* fw = fopen("text1.txt", "w");
	if (fw == NULL)
	{
		return;
	}

	for (int i = 0; i < 50; i++)
	{
		//产生随机数 1 ~ 100
		num1 = rand() % 100 + 1;
		num2 = rand() % 100 + 1;
		ch = oper[rand() % 4];

		char buf[64] = { 0 }; //char buf[] = { 0 }; 会出现堆栈溢出错误
		sprintf(buf, "%d %c %d =\n", num1, ch, num2);
		fputs(buf, fw);
	}

	fclose(fw);

}

//解题
void answerQuestion()
{
	FILE* fr = fopen("text1.txt", "r");

	FILE* fw = fopen("text2.txt", "w");

	if (fr == NULL || fw == NULL)
	{
		printf("文件打开失败\n");
		return;
	}

	char buf[64] = { 0 };
	while (fgets(buf, sizeof(buf),fr))
	{
		int num1 = 0;
		int num2 = 0;
		char ch = 0;

		sscanf(buf, "%d %c %d", &num1, &ch, &num2);

		int res = 0;
		switch (ch)
		{
		case '+':
			res = num1 + num2;
			break;
		case '-':
			res = num1 - num2;
			break;
		case '*':
			res = num1 * num2;
			break;
		case '/':
			res = num1 / num2;
			break;
		default:
			break;
		}
		//组装成新的字符串
		char str[64] = { 0 };
		sprintf(str, "%d %c %d = %d\n", num1, ch, num2, res);

		//写入文件
		fputs(str, fw);
	}

	fclose(fr);
	fclose(fw);
}

main函数所在文件(project.c)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include "test.h"

/*
	产生 50 道随机题目,形式如 1 + 1 = 
	出题:出 50 个题目,随机数在 1 ~ 100 之间,运算符 + - * / ,存入到文件中
	解题:利用程序将50 个题目解出,并存入到另一个文件中
*/

int main()
{
	//setQuestion();
	answerQuestion();

	return 0;
}
3. 按照格式化文件 fprintf fscanf
1. fprintf()
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...);
功能:根据参数 format 字符串来转换并格式化数据,然后将结果输出到 stream 指定的文件中,指定出现字符串结束		符 '\0' 为止。当stream参数为stderr时,会将错误信息打印到显示器
参数:
	stream:已经打开的文件
	format:字符串格式,用法和 printf()一样
返回值:
	成功:实际写入文件的字符个数
	失败:-1
#include <stdio.h>

int main() {
    int x = 10, y = 0;
    if (y == 0) {
        fprintf(stderr, "除数不能为0\n");
    } else {
        int result = x / y;
        fprintf(stdout, "结果: %d\n", result);
    }

    return 0;
}
2. fscanf()
#include <stdio.h>
int fscanf(FILE * stream, const char * format, ...);
功能:从 stream 指定的文件读取字符串,并根据参数 format 字符串来转换并格式化数据。
参数:
	stream:已经打开的文件
	format:字符串格式,用法和 scanf()一样
返回值:
	成功:参数数目,成功转换的值的个数
	失败: - 1
3. 案例

写入文件:

struct Hero{
    char name[64];
    int atk;
    int def; 
};

//按照格式化文件方式写入文件
void test01()
{
    struct Hero heroArray[5] = 
    {
        {"刘备", 1299, 1214},
        {"关羽", 1539, 1994},
        {"张飞", 1499, 1864},
        {"曹操", 1699, 1700},
        {"吕布", 1899, 2000},
    };
    
    FILE* fw = fopen("text.txt", "w");
    if(fw == NULL)
    {
        printf("文件打开失败\n");
        return;
    }
    
    int len = sizeof(heroArray) / sizeof(heroArray[0]);
    for(int i = 0; i < len; i++)
    {
        fprintf(fw, "[性别]%s [攻击力]%d [防御力]%d\n", heroArray[i].name, heroArray[i].atk, heroArray[i].def);
    }
    
    fclose(fw);
}

读文件:

void test02()
{
	FILE* fr = fopen("text.txt", "r");
	if(fr == NULL)
	{
		printf("文件打开失败\n");
		return;
	}
	
	struct Hero heroArray[5];
	memset(heroArray, 0, sizeof(heroArray));	
	
	int i = 0;
	while(!feof(fr))
	{
        //heroArray[i].name 字符数组的数组名,即该数组元素的首地址
		fscanf(fr, "[性别]%s [攻击力]%d [防御力]%d\n", heroArray[i].name, &heroArray[i].atk, &heroArray[i].def);
		i++;
	}
	
	int len = sizeof(heroArray) / sizeof(heroArray[0]);
	for(int i = 0; i < len; i++)
	{
		printf("name = %s atk = %d def = %d\n", heroArray[i].name, heroArray[i].atk, heroArray[i].def);
	}
	
	fclose(fr);
}
4. 按块读写文件 fread fwrite
1. fread()
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
	ptr:存放读取出来数据的内存空间
	size: size_t 为 unsigned int 类型,此参数指定读取文件内容的块数据大小
	nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
	stream:已经打开的文件指针
返回值:
	成功:实际成功读取到内容的块数,如果此值比 nmemb 小,但大于 0,说明读到文件的结尾。
	失败:0
2. fwrite()
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容
参数:
	ptr:准备写入文件数据的地址
	size: size_tunsigned int 类型,此参数指定写入文件内容的块数据大小
	nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
	stream:已经打开的文件指针
返回值:
	成功:实际成功写入文件数据的块数目,此值和 nmemb 相等
	失败:0

案例1

写文件:

struct Hero{
    char name[64];
    int atk;
    int def; 
};

void test01()
{
    FILE* fw = fopen("text.txt", "w");
    if(fw == NULL)
    {
        printf("文件打开失败\n");
        return;
    }
    
    struct Hero heroArray[5] = 
    {
        {"刘备", 1299, 1214},
        {"关羽", 1539, 1994},
        {"张飞", 1499, 1864},
        {"曹操", 1699, 1700},
        {"吕布", 1899, 2000},
    };
    
    int len = sizeof(heroArray) / sizeof(heroArray[0]);
//    for(int i = 0; i < len; i++)
//    {
//        fwrite(&heroArray[i], sizeof(struct Hero), 1, fw);
// 	  }
    //一次性写入
    // sizeof(heroArray[0]) ==  sizeof(struct Hero),
    fwrite(heroArray, sizeof(heroArray[0]), 5, fw);
    
    //关闭文件
    fclose(fw);
}

读文件:

void test02()
{
    FILE* fr = fopen("text.txt", "r");
    if(fr == NULL)
    {
        printf("文件打开失败\n");
        return;
    }
    
    struct Hero heroArray[5];
    memset(heroArray, 0, sizeof(heroArray));
    int len = sizeof(heroArray) / sizeof(struct Hero);
//    for(int i = 0; i < len; i++)
//    {
//        fread(&heroArray[i], sizeof(struct Hero), 1, fr);
//    }
    //一次性读取
    fread(heroArray, sizeof(struct Hero), 5, fr);
    
    for(int i = 0; i < len; i++)
    {
        printf("name = %s atk = %d def = %d\n", heroArray[i].name, heroArray[i].atk, heroArray[i].def);
    }
    
    fclose(fr);
}
3. 案例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<time.h>

void sortArray(int[], int);

//需求:生成 100 随机数,进行排序
void test1()
{
	//随机数种子
	srand((size_t)time(NULL));

	FILE* fw = fopen("随机数.txt", "w");
	if (fw == NULL)
	{
		printf("文件打开失败\n");
	}

	for (int i = 0; i < 100; i++)
	{
		fprintf(fw, "%d\n", rand() % 1000 + 1);  //1 ~ 1000
	}

	fclose(fw);
}

//排序随机数并生成文件
void test2()
{
	FILE* fr = fopen("随机数.txt", "r");;
	FILE* fw = fopen("排序随机数.txt", "w");
	if (!fr || !fw)
	{
		printf("文件打开失败\n");
	}

	int arr[100] = { 0 };
	for (int i = 0; i < 100; i++)
	{
		fscanf(fr, "%d\n", &arr[i]);  //将 fr 指向的文件的内容格式化输出到 arr 中
	}

	sortArray(arr, 100);

	//将排序好的数据存放到 fw 指向的文件中
	for (int i = 0; i < 100; i++)
	{
		fprintf(fw, "%d\n", arr[i]);
	}

	fclose(fw);
	fclose(fr);
}

//冒泡排序
void sortArray(int arr[], int len)
{
	for (int i = 0; i < len - 1; i++)
	{
		for (int j = 0; j < len - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}

int main()
{
	//test1();
	test2();

	return 0;
}

4. 文件的随机读写

1. fseek()

**移动文件流(文件光标)的读写位置。**seek 寻求

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
功能:移动文件流(文件光标)的读写位置。
参数:
	stream:已经打开的文件指针
	offset:根据 whence 来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于 whence 往			右移动,如果是负数,则相对于 whence 往左移动。如果向前移动的字节数超过了文件开头则出错回,			  如果向后移动的字节数超过了文件末尾,再次写入时将 	增大文件尺寸。
whence:其取值如下:
			SEEK_SET:从文件开头移动 offset 个字节
			SEEK_CUR:从当前位置移动 offset 个字节
			SEEK_END:从文件末尾移动 offset 个字节
返回值:
	成功:0
	失败:-1
2. ftell()

获取文件流(文件光标)的读写位置。

#include <stdio.h>
long ftell(FILE *stream);
功能:获取文件流(文件光标)的读写位置。
参数:
	stream:已经打开的文件指针
返回值:
	成功:当前文件流(文件光标)的读写位置
	失败:-1

对文件的读或者写共享一个文件指针,所以不能同时对文件进行读写。

  • 要么在写完之后关闭文件,重新打开文件进行读取
  • 要么使用 fseek() 将文件指针移到文件的开头进行读取操作
void test01()
{
    //这里第二个参数如果写 “w”, 文件指针不会因为调用了 fseek()函数而移动,如果要使文件指针移动,必须写 		“w+”
    //另外,a模式是追加方式写文件,fseek是无效的 
	FILE* fw = fopen("text.txt", "w+"); 
	if(fw == NULL)
	{
		printf("文件打开失败\n");
        return;
	}
    
    fputs("hello world", fw);
#if 0    
    fclose(fw);
    fw = fopen("text.txt", "r");
#else
    fseek(fw, 0, SEEK_SET); 	
    //rewind(fw); //功能:将文件光标置首
#endif
    
    char buf[32] = {0};
    fgets(buf, sizeof(buf), fw);
    printf("%s\n", buf);
    
    fclose(fw);
}

void test02()
{
    FILE* fr = fopen("text.txt", "r");
    //移动光标,到文件末尾
    fseek(fr, 0, SEEK_END);
    
    long len = ftell(fr);
    
    printf("len = %d\n", len); //文件大小 11字节
    
    //将文件中的数据读取出来,并放入到堆区中的数组中
    char * p = (char *)malloc(len + 1); // +1 预留 \0
    memset(p, 0, len + 1);
    //将光标置首
    rewind(fr);
    //按块读取数据
    fread(p, len, 1, fr);
    //打印字符串
    printf("%s\n", p);
    
    //释放字符数组
    if(p != NULL)
    {
        free(p);
        p = NULL;
    }
    
    fclose(fr);
}

int main()
{
    //test01();
    test02();
}
3. rewind()

把文件流(文件光标)的读写位置移动到文件开头

#include <stdio.h>
void rewind(FILE *stream);
功能:把文件流(文件光标)的读写位置移动到文件开头。
参数:
stream:已经打开的文件指针
返回值:
无返回值

与 fseek(fp, 0, SEEK_SET);功能相同

5. window 和 Linux文本文件区别

  • b 是二进制模式的意思,b 只是在 Windows 有效,在 Linux 用 r 和 rb 的结果是一样的

  • Unix 和 Linux 下所有的文本文件行都是\n 结尾,而 Windows 所有的文本文件行都是\r\n 结尾

  • 在 Windows 平台下,以“文本”方式打开文件,不加 b

    • 当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n”
    • 当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入
    • 以"二进制"方式打开文件,则读\写都不会进行这样的转换
  • 在 Unix/Linux 平台下,“文本”与“二进制”模式没有区别,“\r\n” 作为两个字符原样输入输出

判断文本文件是 Linux 格式还是 Windows 格式:

#include<stdio.h>
int main(int argc, char **args)
{
	if (argc < 2)
	return 0;
	FILE *p = fopen(args[1], "rb");
	if (!p)
	return 0;
    
	char a[1024] = { 0 };
	fgets(a, sizeof(a), p);
	int len = 0;
	while (a[len])
	{
		if (a[len] == '\n')
		{
			if (a[len - 1] == '\r')
			{
				printf("windows file\n");
			}		
			else
			{
				printf("linux file\n");
			}
		}
		len++;
	}
	fclose(p);
	return 0;
}

6. 获取文件状态

1. stat()

获取文件状态信息

不需要打开文件,就能获取文件信息(没有用到FILE 类型指针)

包含头文件:<sys/types.h> <sys/stat.h>

#include <sys/types.h>
#include <sys/stat.h>
int stat(const char *path, struct stat *buf);
功能:获取文件状态信息
参数:
	path:文件名
	buf:保存文件信息的结构体 // 需要程序员自己定义,这个函数将为文件信息返回到该结构体中,调用该结构体的成员就可以得到该文件的相关信息
返回值:
	成功:0
	失败-1

这里面的所有参数在Linux下是可行的:

struct stat {
	dev_t st_dev; //文件的设备编号
	ino_t st_ino; //节点
	mode_t st_mode; //文件的类型和存取的权限
	nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为 1
	uid_t st_uid; //用户 ID
	gid_t st_gid; //组 ID
	dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
	off_t st_size; //文件字节数(文件大小)
	unsigned long st_blksize; //块大小(文件系统的 I/O 缓冲区大小)
	unsigned long st_blocks; //块数
	time_t st_atime; //最后一次访问时间
	time_t st_mtime; //最后一次修改时间
	time_t st_ctime; //最后一次改变时间(指属性)
};

案例:

void test()
{
	struct stat myStat; //包含头文件 #include <sys/types.h>  #include <sys/stat.h>
	stat("text.txt", &myStat);
	
	printf("文件的大小:%d\n", myStat.st_size);
    
    //获取atime 最后访问时间
    char * time = ctime(&myStat.st_atime);  //包含头文件 <time.h>
    //printf("atime = %s", time);
    
    //处理换行符 方式1
    char atime[1024] = {0};
    /* 	strcpy(atime, time);
    	atime[strlen(atime) -1] = '\0';
    	printf("atime = %s\n", atime);;
    */
    
    //处理换行符 方式2
    strncpy(atime, time, strlen(time) -1);
    printf("atime = %s\n", atime);
    
    //获取 mtime  最后修改时间
    char * time2 = ctime(&myStat.st_mtime);
    char mtime[1024] = {0};
    strncpy(mtime, time2, strlen(time2) - 1);
    printf("mtime = %s\n", mtime);
}

7. 删除文件,重命名文件

1. remove
#include <stdio.h>
int remove(const char *pathname);
功能:删除文件
参数:
	pathname:文件名
返回值:
	成功:0
	失败:-1
int i = remove("text.txt");
if(i == 0)
{
    printf("删除文件成功\n");
}
else
{
    printf("删除文件失败\n");
}
2. rename
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
功能:把 oldpath 的文件名改为 newpath
参数:
	oldpath:旧文件名
	newpath:新文件名
返回值:
	成功:0
	失败: - 1
// 重命名 rename(旧名,新名)
rename("text1.txt", "text2.txt");

8. 文件缓冲区

1. 概念

ANSI C 标准采用“缓冲文件系统”处理数据文件。

所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区,从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去

如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。

2. 磁盘文件的存取

输出缓冲区:out (从内存输出到磁盘)

输入缓冲区:in (从磁盘输入到内存)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

缓冲区:提高硬件寿命,提高读写效率

2. 刷新缓冲区 fflush()

刷新缓冲区:数据从缓冲区到磁盘的过程

满刷新:

  • 缓冲区中数据存满后,向磁盘写入数据

行刷新:

  • 遇到换行符,将文件中的数据写入到磁盘中

强制刷新:

  • fflush 强制将缓冲区中的数据写入到磁盘中

关闭刷新:

  • fclose 关闭文件的时候,将缓冲区中最后剩余的数据写入到磁盘中

无缓冲:

  • perror 缓冲区中只要有数据,立即刷新
#include <stdio.h>
int fflush(FILE *stream);
功能:更新缓冲区,让缓冲区的数据立马写到文件中。
参数:
	stream:文件指针
返回值:
	成功:0
	失败:-1

9. 文件相关案例

案例一

需求:

  1. 有欢迎页面
  2. 记录新的比赛分数
  3. 查看往届记录
  4. 清空比赛记录
  5. 退出程序
#define _CRT_SECURE_NO_WARNINGS //这个宏定义最好要放到.c 文件的第一行
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define FILENAME  "score.txt"

//记录文件是否为空的标志
int fileEmpty = 1;  // 1 为空

//显示菜单
void showMenu()
{
	printf("******************************\n");
	printf("******* 欢迎使用系统   *******\n");
	printf("******* 1.记录比赛得分 *******\n");
	printf("******* 2.查看往届记录 *******\n");
	printf("******* 3.清空比赛记录 *******\n");
	printf("******* 4.退出当前程序 *******\n");
}

//记录新的分数
void setScore()
{
	printf("请输入比赛的分数:\n");
	double score = 0;
	scanf("%lf", &score);

	FILE* fw = fopen(FILENAME, "a");
	if (fw == NULL)
	{
		printf("文件打开失败\n"); 
		return;
	}

	//格式化方式写入
	fprintf(fw, "%lf\n", score);

	fclose(fw);

	fileEmpty = 0; //添加数据

	printf("分数记录成功\n");
	system("pause"); //按任意建
	system("cls"); //清屏
}

//显示所有分数
void showScore()
{
	// 如果无记录,直接return
	if (fileEmpty == 1)
	{
		printf("当前记录为空\n");
		system("pause");
		system("cls");
		return;
	}
	
	FILE* fr = fopen(FILENAME, "r");
	if (fr == NULL)
	{
		printf("文件打开失败\n");
		return;
	}

	int i = 1;
	while (!feof(fr))
	{
		double score = 0;
		//防止 \n 问题:格式化方式与 fprintf() 相同即可
		//fscanf(fr, "%lf\n", &score);

		int ret = fscanf(fr, "%lf", &score);
		if (ret == -1) 
		{
			break;
		}
		
		printf("第%d届的分数为:%lf\n", i++, score);
	}
	
	fclose(fr);
	system("pause");
	system("cls");
}

//清除所有分数
void clearScore()
{
	// 如果无记录,直接return
	if (fileEmpty == 1)
	{
		printf("当前记录为空\n");
		system("pause");
		system("cls");
		return;
	}

	printf("是否确定清空?\n");
	printf("1.确定\n");
	printf("2.取消\n");

	int res = 0;

	scanf("%d", &res);
	if (res == 1)
	{
		//清空
		FILE* fp = fopen(FILENAME, "w"); //以 “w”方式,如果文件存在则清空,不存在则创建
		fclose(fp);

		fileEmpty = 1; 

		printf("清空完毕\n");
	}
	
	system("pause");
	system("cls");
}

//初始化文件标志
void initFlag()
{
	FILE* fp = fopen(FILENAME,"r");
	if (fp == NULL)
	{
		printf("文件打开失败\n");
		return;
	}

	char ch = 0;
	ch =fgetc(fp);
	if (ch == EOF)
	{
		fileEmpty = 1; //为空
	}
	else
	{
		fileEmpty = 0;  
	}
}

int main()
{
	//初始化文件是否为空的标志
	initFlag();

	//接收用户选择的变量
	int flag = 0;
	
	while (1)
	{
		showMenu();

		printf("请输入您的选择:\n");
		scanf("%d", &flag);
		switch (flag)
		{
		case 1:
			setScore();
			break;
		case 2:
			showScore();
			break;
		case 3:
			clearScore();
			break;
		case 4:
			printf("欢迎您下次使用\n");
			system("pause");
			exit(0);
			break;
		default:
			system("cls");
			break;
		}
	}

	return 0;
}
案例二

3.预处理

预处理的基本概念

C 语言对源程序处理的四个步骤:预处理、编译、汇编、链接。

预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行

的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定

的符号为下一步的编译做准备工作。

文件包含指令(#include)

文件包含处理

“文件包含处理”是指一个源文件可以将另外一个文件的全部内容包含进来。C语言提

供了#include 命令用来实现“文件包含”的操作。

#incude<>和#include""区别

  • “” 表示系统先在 file1.c 所在的当前目录找 file1.h,如果找不到,再按系统指定的目录检索。

  • < > 表示系统直接按系统指定的目录检索。

注:

  1. #include <>常用于包含库函数的头文件;

  2. #include ""常用于包含自定义的头文件;

  3. 理论上#include 可以包含任意格式的文件(.c .h 等) ,但一般用于头文件的包含;

宏定义
无参数的宏定义(宏常量)

如果在程序中大量使用到了 100 这个值,那么为了方便管理,我们可以将其定义为:const int num = 100;

但是如果我们使用 num 定义一个数组,在不支持 c99 标准的编译器上是不支持的(伪常量),因为 num 不是一个编译器常量,如果想得到了一个编译器常量,那么可以使用:#define num 100

在编译预处理时,将程序中在该语句以后出现的所有的 num 都用 100 代替。这种方法使用户能以一个简单的名字代替一个长的字符串,在预编译时将宏名替换成字符串的过程称为“宏展开”。宏定义,只在宏定义的文件中起作用。

#define PI 3.1415
void test(){
	double r = 10.0;
	double s = PI * r * r;
	printf("s = %lf\n", s);
}

说明:

  1. 宏名一般用大写,以便于与变量区别;
  2. 宏定义可以是常数、表达式等;
  3. 宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错;
  4. 宏定义不是 C 语言,不在行末加分号;
  5. 宏名有效范围为从定义到本源文件结束;(可以看作其没有作用域的限制,在函数体内定义在另一个函数中依旧能够使用)
  6. 可以用#undef 命令终止宏定义的作用域;(#undef PI)
  7. 在宏定义中,可以引用已定义的宏名;
  8. 宏常量和const常量的对比在基础篇
带参数的宏定义(宏函数)

在项目中,经常把一些短小而又频繁使用的函数写成宏函数,这是由于宏函数没有普通函数参数压栈、跳转、返回等的开销,可以调高程序的效率。(以空间换时间)

宏通过使用参数,可以创建外形和作用都与函数类似地类函数宏(function-like macro). 宏的参数也用圆括号括起来。

#define SUM(x,y) (( x )+( y ))
void test(){
	//仅仅只是做文本替换 下例替换为 int ret = ((10)+(20));
	//不进行计算
	int ret = SUM(10, 20);
	printf("ret:%d\n",ret);
}

注意:

  1. 宏的名字中不能有空格,但是在替换的字符串中可以有空格。ANSI C 允许在参数列表中使用空格;

  2. 用括号括住每一个参数,并括住宏的整体定义。

  3. 用大写字母表示宏的函数名。

  4. 如果打算宏代替函数来加快程序运行速度。假如在程序中只使用一次宏对程序的运行时间没有太大提高

宏函数的去缺陷
//缺陷1:必须加括号保证运算的完整性
#define ADD(x,y) ((x)+(y))
void test(){
    int a = 10;
    int b = 20;
    int ret = ADD(a, b) * 20;
    cout << ret << endl;
}

//缺陷2;即使加了括号,有些运算符依然与预期不符
#define COM(a,b) (((a)<(b))?(a):(b))
//普通函数:不会出现与预期结果不符的问题
void myCom(int a, int b){
    int ret = a < b ? a:b;
    cout << "ret = " << ret << endl;
}

void test02(){
    int a = 10;
    int b = 20;
    
    myCom(++a, b);
    //int ret = COM(++a, b); //预期是 11 结果是 12 (((++a)<(b))?(++a):(b))
}

故c++中采用了一种内联函数(inline )见基础篇 函数一章

条件编译(头文件保护)

基本概念

一般情况下,源程序中所有的行都参加编译。但有时希望对部分源程序行只在满足一定

条件时才编译,即对这部分源程序行指定编译条件

作用

防止头文件被重复包含引用

#ifndef _SOMEFILE_H
#define _SOMEFILE_H
//需要声明的变量、函数
//宏定义
//结构体
#endif
#ifndef SALES_DATA_H
#define SALES_DATA_H

#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};

#endif
一些特殊的预定宏

C 编译器,提供了几个特殊形式的预定义宏,在实际编程中可以直接使用,很方便

// __FILE__ 宏所在文件的源文件名
// __LINE__ 宏所在行的行号
// __DATE__ 代码编译的日期
// __TIME__ 代码编译的时间
void test()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
}

4. 库的封装和使用

库的基本概念

库是已经写好的、成熟的、可复用的代码。每个程序都需要依赖很多底层库,不可能每个人的代码从零开始编写代码,因此库的存在具有非常重要的意义。

在我们的开发的应用中经常有一些公共代码是需要反复使用的,就把这些代码编译为库文件。

库可以简单看成一组目标文件的集合,将这些目标文件经过压缩打包之后形成的一个文件。像在 Windows 这样的平台上,最常用的 c 语言库是由集成按开发环境所附带的运行库,这些库一般由编译厂商提供

windows下静态库创建

详见c高级编程讲义第12章

linux下静态库(.a)创建和动态库(.so)创建见linux系统编程笔记

1 静态库的创建(.lib)
  1. 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入 staticlib。点击确定。

  2. 在解决方案资源管理器的头文件中添加,mylib.h 文件,在源文件添加 mylib.c 文件(即实

现文件)。

  1. 在 mylib.h 文件中添加如下代码:

    #ifndef TEST_H
    #define TEST_H
    
    int myadd(int a,int b);
    
    #endif
    
  2. 在 mylib.c 文件中添加如下代码:

    #include"test.h"
    int myadd(int a, int b){
    	return a + b;
    }
    
  3. 配置项目属性。因为这是一个静态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“静态库(.lib)。

  4. 编译生成新的解决方案(点击VS中的生成按钮),在 Debug 文件夹下(该文件的上级目录下的Debug文件)会得到 mylib.lib (对象文件库),将该.lib文件和相应头文件(.h)给用户(CV大法,将其添加到项目中,右键项目名,点击添加),用户就可以使用该库里的函数了。

windows 下动态库创建和使用

5. 递归函数

本质:函数自身调用自身

注意:递归函数必须有结束条件,即递归函数出口

6. 面向接口编程

回调函数

c++对c的加强和拓展

实用性增强

  • C语⾔中的变量都必须在作⽤域开始的位置定义!!

  • C++中更强调语⾔的“实用性”,所有的变量都可以在需要使用时再定义。(局部变量必须初始化,否则会有意想不到的bug。全局变量自动初始化为 0)

1.全局变量检测增强

  • ​ 在C语⾔中,重复定义多个同名的全局变量是合法的

  • ​ 在C++中,不允许定义多个同名的全局变量。

​ C语⾔中多个同名的全局变量最终会被链接到全局数据区的同⼀个地址空间上

​ int g_var;

​ int g_var = 1;

​ C++直接拒绝这种⼆义性的做法

//c语言中可行
#include<stdio.h>
int a;
int a = 10;

int main(){
	return 0;
}

//c++中不可行, 报错:重定义
#include<iostream>
int a;
int a = 10;

int main(){
    return 0;
}

2. 函数检测增强

//c语言中,返回值没有检测,形参类型没有检测,函数调用参数个数没有检测
//这种代码在c语言中是没有错的
getRectS(w, h){
    return  w *h;
}
int main(){
    printf("%d\n", getRectS(10, 10, 10));
}

//c++中,会进行 返回值检测,形参类型检测,函数调用参数个数检测。
//像上面那样的代码在c++中会报错
int getRectS(int w, int h){
    return w * h;
}
int main(){
    printf("%d\n", getRectS(10, 10));
}

C++中所有的变量和函数都必须有类型,C语⾔中的默认类型在C++中是不合法的

//	函数f的返回值是什么类型,参数⼜是什么类型?函数g可以接受多少个参数?
f(i)	
{	
	printf("i	=	%d\n",	i);	
}	
g()	
{	
	return 5;	
}	
int	main(int	argc,	char	*argv[])	
{
	f(10);	
	printf("g()	=	%d\n",	g(1, 2,	3, 4,5));	
	getchar();	
	return 0;	
}

在C语言中

  • int f( );表示返回值为int,接受任意参数的函数

  • int f(void);表示返回值为int的无参函数

在C++中

  • int f( );和int f(void)具有相同的意义,都表示返回值为int的无参函数

C++更加强调类型,任意的程序元素都必须显示指明类型

3. 类型转换检测加强

//c语言中,使用malloc分配内存时,可以不用强制类型转换
void test(){
    char * p  = malloc(sizeof(char));
}

//c++中,使用malloc分配内存时,必须使用强制类型转换 
void test(){
    char * p = (char *)malloc(sizeof(char)); 

4. struct 增强

  • c语言, 结构体中 不可以有函数;创建结构体变量时,必须加关键字 struct
  • c++, 结构体中 可以有函数;创建结构体变量时,可以不加关键字 struct
//c语言, 结构体中 不可以有函数;创建结构体变量时,必须加关键字 struct
struct Person{
    int age;
    //void fun();
}

int main(){
    struct Person p;
    p.age = 100;
}

//c++, 结构体中 可以有函数;创建结构体变量时,可以不加关键字 struct
struct Person{
    int age;
    void fun1(){
        age++;
    }
}

int main(){
    struct Person p;
    p.age = 18;
    p.fun1();
    cout << "p的age = " << p.age << endl;
}

5. bool 类型

  • ​ C++中的布尔类型

​ C++在C语⾔的基本类型系统之上增加了bool

​ C++中的bool可取的值只有true和false

理论上bool只占⽤⼀个字节,如果多个bool变量定义在⼀起,可能会各占⼀个bit,这取决于编译器的实现

bool类型只有true(⾮0)和false(0)两个值。C++编译器会在赋值时将⾮0值转换为true(1),0值转换为false(0)

  • true代表真值,编译器内部⽤1来表⽰

  • false代表⾮真值,编译器内部⽤0来表⽰

//c语言中没有 bool类型

bool flag = true; //ture -> 1 false -> 0
cout << sizeof(flag) << endl;
flag = 100;
cout << flag << endl; //1

6. 三目运算符增强

1)C语言返回变量的值 C++语言是返回变量本身

  • C语言中的三目运算符返回的是变量,不能作为左值使用

  • C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方

2)注意:三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用

​ (a < b ? 1 : b )= 30;

3)C语言如何支持类似C++的特性呢?

​ 当左值的条件:要有内存空间;

​ C++编译器帮助程序员取了一个地址而已

// c语言返回的是值
void test(){
    int a = 10;
    int b = 20;
    //a > b ? a : b = 100; //c语言下,返回的是值:20 == 100(报错)
    *(a > b ? &a : &b) = 100; 
    
    printf("a = %d\n", a);
    printf("b = %d\n", b);
}

//c++
void test(){
    int a = 10;
    int b = 20;
    
    a > b ? a : b = 100; // c++返回的是变量 b = 100 (a > b 条件不成立,返回 b = 100)
    a < b ? a : b = 100; // (a < b 条件成立,返回 a)
    (a < b ? a : b) = 100; // (相当于 a == 100)
    
    printf("a = %d\n", a);
    printf("b = %d\n", b);
}

7. const增强

c语言下:

  • 全局const 直接修改 失败 间接修改 语法通过,运行失败
  • 局部const 直接修改 失败 间接修改 成功

c++下:

  • 全局const 直接修改 失败 间接修改 语法通过,运行失败
  • 局部const 直接修改 失败 间接修改 语法通过,结果未更改。失败
//c语言
//全局 const
const int m_A = 100; //在常量区,修改失败
void test(){
    //m_A = 200;
    //int *p = &m_A;
    //*p = 200;
    
    //局部 const
    const int m_B = 100; //分配到栈上
    //m_B = 200;
    int *p = &m_B;
    *p = 200;
    printf("%d\n", m_B); //200
    
    int arr[m_B]; // c语言下,m_B 是伪常量,不可以初始化数组
}


//c++
//全局 const
const int m_A = 100;
void test01(){
    //m_A = 200;
    //int *p = &m_A;
    //*p = 200;
    
    //局部 const
    const int m_B = 100;
    //m_B = 200;
    int *p = (int*)&m_B; //会创建临时变量temp,即p指向的空间是temp的空间,而不是 m_B的空间,自然改不了m_B
    *p = 200;
    printf("%d\n", m_B);
    
    int arr[m_B]; // c++下const修饰的变量称为常量,可以初始化数组
}
c/c++全局const的区别
  • c语言下,const修饰的全局变量默认是外部链接属性

    在某个文件中创建了const修饰的全局变量(const int a = 10),在另一个文件中(不包含该头文件),只要声明了该全局变量(extern const int a),就可以使用它

  • c++中,const修饰的全局变量默认是内部链接属性

    在某个文件中创建了const修饰的全局变量(const int a = 10),在另一个文件中(不包含该头文件),声明了该全局变量(extern const int a),也不能使用它。

    必须在创建该变量时用extern修饰(extern const int a = 10),才能在另一个文件中声明后使用该变量。

    默认状态下,const对象仅在文件内有效,如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字

const分配内存的情况
  • 对const 变量取地址,会分配临时内存

    void test(){
    	const int a = 10;
    	int* p = (int *)&a; //底层进行的操作:使用临时变量 int temp = a; int *p = (int *)&temp;
        *p = 1000; //修改的是临时变量 temp 的值
        cout << a << endl; //10
    }
    
  • 使用普通变量初始化 const 变量,分配内存

    void test(){
    	int a = 10;
    	const int b = a; 
    	int * p = (int *)&b;
    	*p = 1000;
    	cout << "b = " << b << endl; //1000
    }
    
  • 自定义数据类型

    struct Person{
        string name; //包含头文件 <string>
        int age;
    }
    void test(){
        const Person p;
        //p.age = 10;
        
        Person *pp = (Person)&p;
        (*pp).name = "tom";
        pp->age = 10;
        cout << "姓名:" << p.name << "年龄:" << p.age << endl;
    }
    
尽量用const代替 #define
  • 节省空间,见基础篇补充1

  • 用 #define 定义的常量,预处理时就会发生宏替换,出错时不好排查

  • 用 #define 定义的常量,没有数据类型;const定义的变量有数据类型

  • #define不重视作用域,默认为从定义处到文件末尾,使用#undef可以卸载#define创建的变量。

    而 const 有作用域

C语言中的const变量

C语言中const变量是只读变量,有自己的存储空间

C++中的const常量

可能分配存储空间,也可能不分配存储空间

​ 当const常量为全局,并且需要在其它文件中使用,会分配存储空间

​ 当使用&操作符,取const常量的地址时,会分配存储空间

​ 当const int &a = 10; const修饰引用时,也会分配存储空间

8. 真正的枚举

  • c 语言中枚举本质就是整型,枚举变量可以用任意整型赋值。

  • c++中枚举变量, 只能用被枚举出来的元素初始化。

枚举类型应用说明:

​ 对枚举元素按常量处理,不能对它们赋值。例如,不能写:SUN = 0;

​ 枚举元素具有默认值,它们依次为: 0,1,2,…。

​ 也可以在声明时另行指定枚举元素的值,如:
​ enum Weekday{SUN=7,MON=1,TUE,WED,THU,FRI,SAT};

​ 枚举值可以进行关系运算。

​ 整数值不能直接赋给枚举变量,如需要将整数赋值给枚举变量,应进行强制类型转换。

#include<iostream>
using namespace	std;	
enum season{
SPR,SUM,AUT,WIN
};	

int	main()	
{	
	enum season s = SPR;
    // s = 0; // error,但是c语言可以通过
    s = SUM;
    cout << "s = " << s << endl; //1
    
    return 0;
}
9. 类型限定符

见基础篇 2.9

10. 断言

assert宏(在 assert.h 头文件中定义)用于测试表达式的值。用于调试程序

  • 表达式的值为假,则 assert 输出错误信息,并调用 abort() 以结束程序的执行

  • 表达式的值为真,则继续执行后面的语句。

    #include <assert.h>
    
    int main(){
        int x;
        cin >> x;	// 30
        assert(x < 10);
        cout << "x = " << x endl;
    }
    

11. string 和 const char *

  • const char *可以隐式转换为 string
  • string 不能隐私转换为 const char *。但string里面有一个方法 c_str();可以实现该操作 str.c_str();

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值