C++核心学习笔记(第三阶段)

1.内存分区模型

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

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

内存四区意义:

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

1.1 程序运行前

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

代码区:

  • 存放 CPU 执行的机器指令
  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
  • 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

​ 全局区:

  • 全局变量和静态变量存放在此
  • 全局区还包含了常量区, 字符串常量和其他常量也存放在此
  • 该区域的数据在程序结束后由操作系统释放
//全局变量(写在函数体外)
int a1 = 10;
int b1 = 10;

//const修饰的全局变量,即全局常量
const int ca = 10;
const int cb = 10;

int main(){
	//全局区:存放全局变量、静态变量、常量

	//创建普通局部变量
	int a = 10;
	int b = 10;
	cout<<"局部变量a的地址为:"<<(int)&a<<endl;
	cout<<"局部变量b的地址为:"<<(int)&b<<endl;

	cout<<"全局变量a1的地址为:"<<(int)&a1<<endl;
	cout<<"全局变量b1的地址为:"<<(int)&b1<<endl;

	//静态变量:在普通变量之前加上static
	static int s_a = 10;
	static int s_b = 10;
	cout << "静态变量s_a地址为: " << (int)&s_a << endl;
	cout << "静态变量s_b地址为: " << (int)&s_b << endl;

	//常量:字符串常量 和 const修饰的变量
	//字符串常量
	cout << "字符串常量地址为: " << (int)&"hello world" << endl;
	
	//const修饰的变量:const修饰的全局变量即全局常量 和 const修饰的局部变量即局部常量
	cout << "全局常量ca地址为: " << (int)&ca << endl;
	cout << "全局常量cb地址为: " << (int)&cb << endl;
	const int c_l_a = 10;
	const int c_l_b = 10;
	cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
	cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;

	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
总结:

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

1.2 程序运行后

栈区

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

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

//栈区数据由编译器管理开辟和释放
//栈区数据注意事项——不要返回局部变量的地址

int* func()
{
	int a=10; //局部变量:存放在栈区,栈区的数据在函数执行完后自动释放
	return &a;//返回局部变量的地址
}

int main() {
	int *p = func();//接收func函数的返回值

	cout << *p << endl;//第一次可以打印正确数字,是因为编译器做了保留
	cout << *p << endl;//第二次这个数据就不再保留了

	system("pause");
	return 0;
}

运行结果:

10
2019673056

堆区

  • 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
  • 在C++中主要利用new在堆区开辟内存。
int* func() {
	//利用new关键字可以将数据开辟到堆区
	//指针:本质也是局部变量,放在栈上;指针保存的数据是放在堆区
	int* p = new int(10); 
	return p;
}

int main() {
	int *p = func();
	cout << *p << endl;
	cout << *p << endl;
    
	system("pause");
	return 0;
}

输出:

10
10

总结:

  • 堆区数据由程序员管理开辟和释放
  • 堆区数据利用 new 关键字进行开辟内存

1.3 new运算符

  • C++中利用 new 操作符在堆区开辟数据
  • 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
  • 利用new创建的数据,会返回该数据对应的类型的指针

语法:new 数据类型

//1.new的基本语法
int* func()
{
	int* p = new int(10); //在堆区创建整型数据;new返回是该数据类型的指针
	return p;
}

void test01()
{
	int *p=func();
	cout<<*p<<endl;
	cout<<*p<<endl;
	//堆区开辟的数据,由程序员手动开辟,手动释放
	//如果想释放堆区的数据,利用关键字 delete
	delete p;
	cout<<*p<<endl; //此时内存已被释放,再次访问就是非法操作,会报错
}

//2.在堆区利用new开辟数组
void test02()
{
	int* arr = new int[10]; //创建10个整型数据的数组,在堆区
	for (int i = 0; i < 10; i++)
	{
		arr[i] = i + 1;//给10个元素赋值为1-10
	}
	for (int i = 0; i < 10; i++)
	{
		cout<<arr[i]<<endl;
	}

	//释放堆区数组(释放数组时要加中括号[])
	delete[] arr;
}
int main()
{
	//test01();
	test02();
	system("pause");
	return 0;
}

2.引用

2.1 引用的基本使用

作用:给变量起别名

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

int main()
{
	int a=10;
	int &b=a;//创建引用
	cout<<a<<endl;
	cout<<b<<endl;
	b=100;
	cout<<a<<endl;

	system("pause");
	return 0;
}

输出:

10
10
100

2.2 引用注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变
int main()
{
	int a=10,c=20;
	//int &b;     报错,因为引用必须初始化
	int &b=a;     //正确
	//int &b=c;   报错,引用在初始化后就不可以改变了

	b=c;//这是赋值操作,而不是更改引用
	cout<<a<<endl;
	cout<<b<<endl;
	cout<<c<<endl;

	system("pause");
	return 0;
}

输出:

20
20
20

2.3 引用做函数参数

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

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

//1.值传递
void s1(int a,int b)
{
	int temp=a;
	a=b;
	b=temp;
	cout<<a<<"   "<<b<<endl;
}

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

//3.引用传递
void s3(int &a,int &b)//此处的a是main函数中的a的别名;b同理
{
	int temp=a;
	a=b;
	b=temp;
}

int main()
{
	int a=10;
	int b=20;
	//s1(a,b);   //值传递,形参不会修饰实参
	//s2(&a,&b); //地址传递,形参会修饰实参
	s3(a,b);     //引用传递,形参会修饰实参
	cout<<a<<" "<<b<<endl;

	system("pause");
	return 0;
}

输出:

10   20  //值传递
20   10  //地址传递
20   10  //引用

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

2.4 引用做函数返回值

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

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

用法:函数调用作为左值

//引用做函数返回值
//1.不要返回局部变量的引用
int & t1()
{
	int a=10;//局部变量存放在四区中的栈区
	return a;
}

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

int main()
{
	//int &ref=t1();
	//cout<<ref<<endl; //第一次输出10,是因为编译器做了保留
	//cout<<ref<<endl; //第二次输出乱码,因为a的内存已经释放

	int &ref2=t2();
	cout<<ref2<<endl;//输出10
	cout<<ref2<<endl;//继续输出10

	t2()=1000;
	//t2返回的是a的引用,在此相当于将1000赋值给a
	//如果函数的返回值是引用,这个函数调用可以作为左值

	cout<<ref2<<endl;//输出1000
	cout<<ref2<<endl;//继续输出1000

	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& ref = a; //自动转换为 int* const ref = &a; 指针常量的指针指向不可改,也说明为什么引用不可更改
	ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
    
	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;
    
	func(a);
	return 0;
}

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

2.6 常量引用

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

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

//常量引用 使用的场景:通常用来修饰形参,防止误操作

//打印数据函数
void showvalue( const int& v) {
	//v = 200;  //加了const后,不可以再修改,因此报错。如果不加const,则实参中的a也将被修改为200
	cout << v << endl;
}

int main() {

	//int a = 10;
	//int& ref = a; 

	//int& ref = 10;          引用本身需要一个合法的内存空间,因此这行错误
	//const int& ref = 10;    加入const就可以了,编译器将代码修改 int temp = 10; const int& ref = temp;
	//ref = 20;               报错,加入const后变为只读,不可以再修改
	
	//函数中利用常量引用防止误操作修改实参
	int a=100;
	showvalue(a);
	
	system("pause");
	return 0;
}

3.函数提高

3.1 函数默认参数

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

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

//函数默认参数
//如果自己传入了数据,则用自己的数据;如果没有,则用默认值
int func(int a,int b=20,int c=30)
{
	return a+b+c;
}

//注意事项:
//1.如果某个位置已经有了默认参数,那么从它往后也都必须有默认值
//例:int func2(int a,int b=1,int c,int d) 将报错,因为b之后的c和d也必须设定默认值

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

int func3(int a=1,int b=2)
{
	return a+b;
}//函数实现


int main() {

	cout<<func(10)<<endl;     //相当于 a=10,b=20,c=30
	cout<<func(10,30)<<endl;  //相当于 a=10,b=30,c=30
	cout<<func3(1,2)<<endl;   //报错;因为func3的声明与实现都有默认参数

	system("pause");
	return 0;
}

3.2 函数占位参数

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

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

//占位参数
void func(int a , int )//第二个相当于用来占位
{
	cout<<"func"<<endl;
}
int main() {
	func(10,10);  //上面已经占位,所以这里也必须传入两个值
	system("pause");
	return 0;
}
//占位参数还可以有默认参数
void func(int a , int =10)//第二个相当于用来占位
{
	cout<<"func"<<endl;
}
int main() {
	func(10);  
	system("pause");
	return 0;
}

3.3 函数重载

3.3.1 函数重载概述

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

函数重载必须满足以下三个条件:

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

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

//函数重载满足条件:
// 1.同一个作用域下(例:func均在全局作用域下)
// 2.函数名称相同(都是func)
// 3.函数参数类型不同 或 个数不同 或 顺序不同

void func()
{
	cout<<"func1"<<endl;
}

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

void func(double a)
{
	cout<<"func3"<<endl;
}

void func(int a,double b)
{
	cout<<"func4"<<endl;
}

void func(double b,int a)
{
	cout<<"func5"<<endl;
}

//注意:函数的返回值不可以作为函数重载的条件,即无法重载仅按返回类型区分的函数
//int func(double b,int a) //会报错
//{
//	cout<<"func5"<<endl;
//}

int main() {
	func();  
	func(10); 
	func(3.14);
	func(10,3.14);
	func(3.14,10);
	system("pause");
	return 0;
}

输出:

func1
func2
func3
func4
func5

3.3.2 函数重载注意事项

  • 引用作为重载条件
  • 函数重载碰到函数默认参数
//函数重载注意事项
//1.“引用”作为重载条件
void fun(int &a)
{
	cout<<"fun1"<<endl;
}
void fun(const int &a)
{
	cout<<"fun2"<<endl;
}

//2.函数重载碰到函数默认参数
void fun3(int a,int b=10)
{
	cout<<"a,b"<<endl;
}

void fun3(int a)
{
	cout<<"a"<<endl;
}

int main() {
	int a=10;
	fun(a);  //输出fun1
	
	fun(10); //调用的是第二个fun函数,输出为fun2;
	//若调用第一个fun,则等同于int &a=10,这是不合法的内存空间,难以通过
	//调用第二个fun,等同于const int &a=10,合法

	//fun3(10);报错,因为当函数重载碰到默认参数,会出现二义性
	fun3(10,20);//输出"a,b"

	system("pause");
	return 0;
}

4.类和对象

  • C++面向对象的三大特性为:封装、继承、多态
  • C++认为万事万物都皆为对象,对象上有其属性和行为

例如

人可以作为对象,属性有姓名、年龄、身高… 行为有走、跑、跳…
车也可以作为对象,属性有轮胎、方向盘、车灯…行为有载人、放音乐、放空调…

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

4.1 封装

4.1.1 封装的意义

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

封装的意义:

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

封装的意义一: 在设计类的时候,属性和行为写在一起,表现事物。

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

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

//封装的意义一:在设计类的时候,属性和行为写在一起,表现事物

//设计一个圆类,求圆的周长,公式:2 * PI * R
const double PI=3.14;

class circle     //class 代表设计一个类,类后面紧跟着的是类的名称
{
public:          //访问权限:公共权限
	int R;	     //属性:一般是变量,在此为半径
	double ZC()  //行为:一般是个函数,获取圆的周长
	{
		return 2*PI*R;
	}
};

int main() {
	//通过圆类,创建具体的圆(对象)
	//实例化:通过一个类创建一个对象的过程
	circle c1;
	c1.R=10;    //给圆对象的属性进行赋值操作
	cout<<"周长: "<<c1.ZC()<<endl;

	system("pause");
	return 0;
}

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

//设计一个学生类
class student
{
public:  //访问权限:公共权限
	
	//类中的属性和行为,统一称为成员
	//属性,也称为成员属性或成员变量
	//行为,也称为成员函数或成员方法

	//属性:一般是变量
	string name;
	int num;
	
	//行为:一般是个函数
	void show()
	{
		cout<<"姓名:"<<name<<"  学号: "<<num<<endl;
	}

	//给姓名赋值
	void setname(string sname)
	{
		name=sname;
	}
};

int main() {	
	student s1;//通过学生类,创建一个具体的学生,即实例化

	//给对象属性进行赋值操作
	s1.name="张三";      //法1
	s1.setname("张三");  //法2,调用了setname函数
	s1.num=01;
	
	s1.show();  //显示学生信息

	student s2;
	s2.name="李四";
	s2.num=02;
	s2.show();

	system("pause");
	return 0;
}

封装的意义二

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

访问权限有三种:

  • public 公共权限: 成员 类内可以访问,类外可以访问 ;
  • protected 保护权限 : 成员 类内可以访问,类外不可以访问;儿子可以访问父亲中的保护内容
  • private 私有权限: 成员 类内可以访问,类外不可以访问;儿子不可以访问父亲的私有内容
class person
{
public:    //公共权限
	string name;

protected: //保护权限
	string car;

private:   //私有权限
	int password;

public:
		void func()
	{
		name="张三";
		car="拖拉机";
		password=12345;
	}	//类内均可以访问
};

int main() {	
	person p1;

	p1.name="李四";  
	//p1.car="奔驰";  //报错,保护权限的内容,类外访问不到
	//p1.password=01; //报错,私有权限内容,类外访问不到
	
	system("pause");
	return 0;
}

4.1.2 struct和class的区别

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

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

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

int main() {

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

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

	system("pause");
	return 0;
}

4.1.3 成员属性设置为私有

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

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

class person{
public:
	void setname(string sname)//设置姓名
	{
		name=sname;
	}
	string getname( )        //获取姓名
	{
		return name;
	}

	int getage()             //获取年龄
	{
		//age=10;            //初始化为10岁
		return age;
	}

	void setage(int sage)    //设置年龄
	{
		if(sage<0||sage>150)
		{
			cout<<"年龄有误!"<<endl;
			return;
		}
		age=sage;
	}

	void setid(int sid)      //设置Id
	{
		id=sid;
	}

private:	
	string name;       //可读可写
	int  age;          //只读
	int id;            //只写
};

int main() {

	person p;
	p.setname("张三");
	//cout<<"姓名为:"<<p.name<<endl; 报错,类外不可访问私有的
	cout<<"姓名为:"<<p.getname()<<endl;

	p.setage(18);
	cout<<"年龄为:"<<p.getage()<<endl;
	
	p.setid(111);//设置id为111
	//cout<<"ID为:"<<p.setid()<<endl;不可以访问,因为只有只写权限

	system("pause");
	return 0;
}

练习案例1:

设计立方体类(Cube),求立方体面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。

//立方体类设计
//1.创建立方体类
//2.设计属性
//3.设计行为获取立方体面积和体积
//4.分别利用全局函数和成员函数 判断两个立方体是否相等

class cube{
public:
	void setl(int l)   //设置长
	{
		length=l;
	}
	int getl( )        //获取长
	{
		return length;
	}

	void setw(int w)   //设置宽
	{
		width=w;
	}
	int getw( )        //获取宽
	{
		return width;
	}

	void seth(int h)   //设置高
	{
		high=h;
	}
	int geth( )        //获取高
	{
		return high;
	}

	int s( )           //获取立方体面积
	{
		return (2*(length*width+length*high+high*width));
	}

	int v()            //获取立方体体积
	{
		return length*width*high;
	}

	//利用成员函数判断两个立方体是否相等
	bool issameclass(cube c)
	{
		if(length==c.getl() && width==c.getw() && high==c.geth())
		{
			return true;
		}
			return false;
	}
private:	
	int length;       
	int width;         
	int high;            
};

//利用全局函数判断两个立方体是否相等
bool issame(cube c1,cube c2)   //判断真假用bool
{
	if(c1.getl()==c2.getl() && c1.getw()==c2.getw() && c1.geth()==c2.geth())
	{
		return true;
	}
		return false;
}

int main() {
	cube c1;
	c1.setl(10);
	c1.setw(10);
	c1.seth(11);

	cout<<"面积:"<<c1.s()<<endl;
	cout<<"体积:"<<c1.v()<<endl;

	cube c2;
	c2.seth(10);
	c2.setl(10);
	c2.setw(10);

	bool result = issame(c1,c2);//利用全局函数判断
	if(result)
	{
		cout<<"全局函数判断结果:c1和c2相等"<<endl;
	}
	else
	{
		cout<<"全局函数判断结果:c1和c2不相等"<<endl;
	}

	result=c1.issameclass(c2);//利用成员函数判断
	if(result)
	{
		cout<<"成员函数判断结果:c1和c2相等"<<endl;
	}
	else
	{
		cout<<"成员函数判断结果:c1和c2不相等"<<endl;
	}

	system("pause");
	return 0;
}

练习案例2:

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

class point{
public:
	void setx(int x1)//设置x坐标
	{
		x=x1;
	}
	int getx()       //获取x坐标
	{
		return x;
	}	
	void sety(int y1)//设置y坐标
	{
		y=y1;
	}
	int gety()       //获取y坐标
	{
		return y;
	}
private:
	int x;
	int y;
};

class circle{
public:
	void setr(int r1)   //设置半径
	{
		r=r1;
	}
	int getr( )         //获取半径
	{
		return r;
	}
	void setcenter(point center1) //设置圆心
	{
		center=center1;
	}
	point getcenter( )           //获取圆心
	{
		return center;
	}
private:	
	int r;       
	
	//在类中,可以让另一个类作为本类中的成员
	point center;//圆心       
};

//判断点和圆的关系
void isincircle(circle &c,point &p)  
{
	//两点间距离的平方
	int distance=
		(c.getcenter().getx()-p.getx()) * (c.getcenter().getx()-p.getx()) +
		(c.getcenter().gety()-p.gety()) * (c.getcenter().gety()-p.gety());
	//半径的平方
	int rdistance=c.getr() * c.getr();

	if( distance == rdistance ){
		cout<<"点在圆上"<<endl;
	}
	else if(distance>rdistance){
		cout<<"点在圆外"<<endl;
	}
	else{
		cout<<"点在圆内"<<endl;
	}
}

int main() {
	//创建圆
	circle c;
	c.setr(10);
	point center;
	center.setx(10);
	center.sety(0);
	c.setcenter(center);

	//创建点
	point p;
	p.setx(10);
	p.sety(9);

	//判断关系
	isincircle(c,p);

	system("pause");
	return 0;
}

4.2 对象的初始化和清理

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

4.2.1 构造函数和析构函数

  • 对象的初始化和清理也是两个非常重要的安全问题;
  • ​ 一个对象或者变量没有初始状态,对其使用后果是未知; 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题;
  • c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作;
  • 对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供。
  • 编译器提供的构造函数和析构函数是空实现。

构造函数:主要作用在于创建对象时为对象分配空间,并对其数据成员赋值,构造函数由编译器自动调用,无须手动调用。

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

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

析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作,如释放给对象分配的内存空间。

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

  1. 析构函数,没有返回值,也不写void
  2. 函数名称与类名相同,在名称前加上符号 “~”
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
//对象的初始化和清理
//1.构造函数 进行初始化操作
class person
{
public:
	person()//构造函数没有返回值,也不用写void;函数名与类名相同
	{
		cout<<"person构造函数的调用"<<endl;
	}

	~person()//析构函数没有返回值,也不用写void
	{
		cout<<"person析构函数的调用"<<endl;
	}
};

//构造和析构都是必须有的实现,如果我们不提供,那么编译器会提供一个空实现的构造和析构
void test01()
{
	//创建对象p的时候,构造函数会自动调用,而且只调用一次
	//这是在栈上的数据,test01执行完毕后就会释放这个对象
	//对象在销毁前,会自动调用析构函数,而且只调用一次
	person p;
}

//2.析构函数 进行清理的操作

int main() {
	test01();

	system("pause");
	return 0;
}

4.2.2 构造函数的分类及调用

两种分类方式:

  • 按参数分为: 有参构造和无参构造
  • 按类型分为: 普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
//构造函数的分类
class person
{
public:
	//1.按照参数分为:无参构造(默认构造)和有参构造
	//2.按照类型分为:普通构造函数和拷贝构造函数
	person()
	{
		cout<<"person无参构造函数的调用"<<endl;
	}
	person(int a)
	{
		age=a;
		cout<<"person有参构造函数的调用"<<endl;
	}

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

	~person()//析构函数 没有返回值,也不用写void
	{
		cout<<"person析构函数的调用"<<endl;
	}
	int age;
};

//调用
void test01()
{
	//1.括号法
	person p1;//默认构造函数的调用
	person p2(10);//有参构造函数的调用
	person p3(p2);//拷贝构造函数的调用
	   
	    //注意事项①:
		//person p1(); 调用默认构造函数时,不要加括号,因为这行代码,编译器会认为是一个函数的声明
	    //void func(); 这是个函数声明的例子,函数名为func,无参数,返回值类型为void
		//因此person p1()会被编译器认为是“无参数,函数名为p1,返回值类型为person”的函数声明,不会认为是在创建对象

	//2.显示法
	person p1;
	person p2=person(10);//调用有参构造
	person p3=person(p2);//调用拷贝构造

	//person(10)称为匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
	//cout<<"aaaa"<<endl;

	//注意事项②:
	//不要利用拷贝构造函数来初始化匿名对象,如下
	//person(p3);,编译器会认为person(p3) 等价于 person p3;

	//3.隐式转换法
	person p4=10;//相当于 person p4=person(10);调用有参构造
	person p5=p4;//调用拷贝构造
}

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

4.2.3 拷贝构造函数调用时机

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

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
class Person {
public:
	Person() 
	{
		cout << "person默认构造函数调用" << endl;
		mAge = 0;
	}
	Person(int age) 
	{
		cout << "person有参构造函数调用" << endl;
		mAge = age;
	}
	Person(const Person & p) 
	{
		cout << "person拷贝构造函数调用" << endl;
		mAge = p.mAge;
	}
	~Person() //析构函数在释放内存之前调用
	{
		cout << "person析构函数的调用" << endl;
	}
public:
	int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() 
{
	Person p1(20);     //p对象已经创建完毕
	Person p2(p1);     //调用拷贝构造函数
}

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

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

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

4.2.4 构造函数调用规则

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

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

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

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

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但依然提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
class Person {
public:
	Person() 
	{
		cout << "person默认构造函数调用" << endl;
	}
	Person(int age) 
	{
		cout << "person有参构造函数调用" << endl;
		mAge = age;
	}
	//Person(const Person & p) 
	//{
	//	cout << "person拷贝构造函数调用" << endl;
	//	mAge = p.mAge;
	//}
	~Person() 
	{
		cout << "person析构函数的调用" << endl;
	}
	int mAge;
};

void test01() 
{
	Person p;                       //会调用默认构造函数
	p.mAge=18;
	Person p2(p);                   //会调用拷贝构造函数

	cout<<"p2年龄"<<p2.mAge<<endl;  //不管在class中我们有没有写拷贝构造函数,调用test01都会输出“p2年龄18”
	                                //如果我们没有写,编译器会自动写好,并对属性进行值拷贝
}  //test01结束后,会调用两次析构函数,分别释放p和p2

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

4.2.5 深拷贝与浅拷贝

4.2.6 初始化列表

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

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

class person {
public:
	//person(int a,int b,int c) //传统的初始化操作,利用构造函数
	//{
	//	ma=a;
	//	mb=b;
	//	mc=c;
	//}

person():ma(10),mb(20),mc(30)//用“初始化列表”来初始化属性;
	   {   }//形式一:属性赋为10,20,30,不可以在test中修改,

person(int a,int b, int c):ma(a),mb(b),mc(c)//形式二:属性可以在test中修改
	   {   }
	int ma;
	int mb;
	int mc;
};

void test01() 
{
	//person p(10,20,30);   
	person p;//创建对象时,就会调用上述构造函数(形式一),并赋值10,20,30
	person p(20,30,50);//调用形式二
	cout<<p.ma<<"  "<<p.mb<<"  "<<p.mc<<endl;  
}  

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

4.2.7 类对象作为类成员

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

例如:

class A { } 
class B
{
    A a;    // B类中有对象A作为成员,A即为为对象成员。
}

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

class phone//手机类
{
public:
	phone(string pName)
	{
		cout<<"phone的构造函数调用"<<endl;
		m_Pname=pName;
	}
	~phone()
	{   
		cout<<"phone的析构函数调用"<<endl;
	}
	string m_Pname;//手机品牌名称
};

class person //人类
{
public:
	person(string name,string pName):m_Name(name),m_Phone(pName)
	{  
		cout<<"person的构造函数调用"<<endl;
	}
	~person()
	{   
		cout<<"person的析构函数调用"<<endl;
	}
	string m_Name;//姓名
	phone m_Phone;//手机
};

//当其他类的对象作为本类成员,构造时候先构造类对象,再构造自身
//析构的顺序与上述相反

void test01() 
{
	person p("张三","华为");   
	cout<<p.m_Name<<"的手机品牌是"<<p.m_Phone.m_Pname<<endl;  
}  

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

输出:

phone的构造函数调用
person的构造函数调用
张三的手机品牌是华为
person的析构函数调用
phone的析构函数调用

4.2.8 静态成员

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

静态成员分为:

  1. 静态成员变量
  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化
  1. 静态成员函数
  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
class person
{
public:
	static void func()
	{
		ma=100;//静态成员函数可以访问 静态成员变量
		//mb=200;报错,因为静态成员函数不可以访问 非静态成员变量
		cout<<"static void func调用"<<endl;
	}
	static int ma;//静态成员变量;类内声明,类外初始化
	int mb;//非静态成员变量

	//静态成员函数也是有访问权限的
private:
	static void func2()
	{
		cout<<"static void func2调用"<<endl;
	}
};

int person::ma=0;//初始化

//两种访问方式
void test01() 
{
	//1.通过对象访问
	person p;
	p.func();

	//2.通过类名访问
	person::func();

	//person::func2();报错,因为类外访问不到私有的静态成员函数
}  

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

输出:

static void func调用
static void func调用

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

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

  • 在C++中,类内的成员变量和成员函数是分开存储的。
  • 只有非静态成员变量才属于类的对象上
class person
{
	int ma;//非静态成员变量,属于类的对象上
	static int mb;//静态成员变量(类内声明,类外初始化),不属于类的对象上
	void func(){}//非静态成员函数,不属于类的对象上
	static void func2(){}//静态成员函数,不属于类的对象上
};
int person::mb=0;

void test01() 
{
	person p;
	cout<<"p所占的内存空间为"<<sizeof(p)<<endl;
	//class中为空时,输出1,即空对象所占内存空间是1
	//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
	//每个空对象也应该有一个独一无二的内存地址
}  

void test02()
{
	person p;
	cout<<"p所占的内存空间为"<<sizeof(p)<<endl;
}
int main() {
	//test01();
	test02();
	system("pause");
	return 0;
}

4.3.2 this指针概念

通过4.3.1我们知道在C++中,成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。那么问题是:这一块代码是如何区分哪个对象调用自己的呢?

c++通过提供特殊的对象指针,this指针,解决上述问题。

  • this指针指向被调用的成员函数所属的对象
  • this指针是隐含每一个非静态成员函数内的一种指针
  • this指针不需要定义,直接使用即可

this指针的用途

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this
class person
{
public:
	person(int age)
	{
		this->age=age;//this指针指向的是 被调用的成员函数所属的对象,即指向的是p1
		              //1.解决名称冲突
	}
	person& personaddage(person &p)//用引用的方式返回本体;用引用的方式传入
	{
		this->age+=p.age;
		return *this;//this是指向p2的指针,而*this指向的就是p2这个对象本体
	}
	int age;
};

void test01() 
{
	person p1(18);
	cout<<"p1的年龄"<<p1.age<<endl;
}  

//2.返回对象本身用 *this
void test02()
{
	person p1(10);
	person p2(10);

	//链式编程思想
	p2.personaddage(p1).personaddage(p1).personaddage(p1);//p2本身是10岁,然后又加了三次10
	cout<<"p2年龄为"<<p2.age<<endl;
}
int main() {
	//test01();
	test02();
	system("pause");
	return 0;
}

输出:

p2年龄为40

4.3.3空指针访问成员函数

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

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

class person
{
public:
	void showclassname()
	{
		cout<<"this is person class"<<endl;
	}
	void showpersonage()
	{
		cout<<"age="<<mage<<endl;//这里的mage相当于this->mage
		//报错原因:传入的指针是空值NULL,空指针无法访问其属性
	}
	int mage;
};

void test01() 
{
	person * p=NULL;
	p->showclassname();
	//p->showpersonage();//报错
}  

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

4.4 友元

4.5 运算符重载

4.6 继承

4.7 多态

5.文件操作

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值