1、内存分区模型
C++在程序执行时, 内存可分为4个区域:
- 代码区: 存放函数体二进制代码,由操作系统进行管理,我们写的所有代码都在这个区域中有体现
- 全局区: 存放全局变量和静态变量以及常量
- 栈区: 由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区: 由程序员分配和释放,如果程序员不释放那么程序结束时由操作系统释放
内存四区的意义: 内存的四个区域,生命周期各不相同,让我们的编程可以更灵活。
四个区域主要可以体现在程序运行前和程序运行后:
1.1程序运行前
在程序编译后,生成exe可执行程序,未执行该程序前可以分为两个区域:
代码区:存放CPU执行的机器指令。
代码区的两个特点:
- 共享,共享的母的是对于频繁执行的程序,只需要在内存中有一份代码即可
- 只读,防止程序意外的修改程序中的代码
全局区:全局变量和静态变量存放在该区域。
全局区还包含了常量区,字符串常量和其他常量同样存放在全局区,该区域的数据在程序结束后由操作系统释放。
其他数据类型是否在全局区可以使用代码进行测试,像下面一段代码是测量常量字符串、const修饰的全局变量、全局变量、static修饰的静态变量是否在同一区域:
#include <iostream>
using namespace std;
int a = 10;
const int c = 10;
int main()
{
cout << (int)&a << endl;
static int b = 20;
cout << (int)&b << endl;
cout << (int)&"zhangsan" << endl;
cout << (int)&c << endl;
return 0;
}
运行结果为:4759552 4759556 4750132 4750128
从这个运行结果其实就能够直观的看出这些数据在同一个区域,因为他们的内存编号转化为十进制之后相差不大;当然,其他区域的数据是否也在全局区或者其他区域这个可以自行测量。
1.2程序运行后
栈区:由编译器自行分配和释放,存放函数的参数,局部变量等
使用栈区时的注意事项:不要返回局部变量的地址(否则会造成野指针问题),栈区数据的开辟由编译器自动释放。
分析一下下面这段程序,这段程序有什么错误?
int * func()
{
int a = 10;
return &a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
这段程序很明显是有问题的,对于函数func中创建的局部变量,在返回主函数之后局部变量的内存空间自动销毁,返回继续进行解引用操作的话势必会造成野指针的问题。
堆区: 由程序员分配和释放,如果程序员不释放,程序结束的时候由操作系统回收释放
C++中怎么在堆区开辟内存?
使用new关键字
1.在堆区开辟一个整型的空间 int* p = new int(10);
2.在堆区开辟一个整型的数组 int* p = new int[10];
当然有数据的开辟,就有数据的销毁,销毁堆区开辟的空间时使用delete关键字;
程序举例:
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; //报错,释放的空间不可访问
return 0;
}
2、引用
2.1引用的基本类型
作用:给变量起别名
语法:数据类型 &别名 = 原名
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
a = 20;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
a和b指向的是同一块内存空间,a的值改变b的值也随之改变。
2.2引用的注意事项
- 引用必须初始化
- 引用初始化之后不能改变
注意:在使用引用的时候,一定不能写成这样
int& b;
b = a;
第一,引用在使用的时候必须进行初始化;
第二,b = a,进行的是赋值操作;
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& test02() {
static int a = 20;
return a;
}
int main() {
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
return 0;
}
函数调用作为左值的时候相当于一个数的别名,同样可以对此数据进行改变。
2.5引用的本质
本质:引用的本质在C++内部是实线是一个指针常量。
我们之前所说的指针变量运用的主要场景是 数据类型* p = &a;
引用的本质其实也是指针,只不过这个指针是一个常量,其中的数据是不能更改的;
也就是const 数据类型 *p = &a;
2.6常量的引用
作用:常量的引用主要是限制数据只是读的,防止对数据进行误操作。
在引用数据的前面加const修饰
//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
//v += 10;
cout << v << endl;
}
//修饰形参,使形参的内容不可以改变
3、函数提高
3.1函数默认的参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名(形参 = 默认值) {}
int sum(int a , int b = 20, int c = 30)
{
return a + b + c;
}
int main()
{
int ret = sum(10, 20);
cout << ret << endl;
return 0;
}
使用函数默认参数时的注意点:
1.如果某个位置开始函数的参数时默认值,那么从这个位置开始从左往右,都需要有默认参数。
2.如果函数的生命有默认值,函数的实现中不能有默认值。
int sum(int a, int b, int c = 10)//这种请款是不允许的
int sum(int a, int b, int c = 10)
{
return a + b + c;
}
int main()
{
int ret = sum(10, 20);
cout << ret << endl;
return 0;
}
3.2函数占位参数
C++中函数的形参列表里面可以有占位参数,用来做占位,调用函数的时候必须填补占位参数的位置。
语法: 返回值类型 函数名(数据类型){}
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "func函数的调用" << endl;
}
int main() {
func(10,10); //占位参数必须填补
return 0;
}
3.3函数重载
3.3.1函数重载的概述
函数重载:函数名可以相同,提高复用性
函数重载需要满足的条件:
- 同一作用域
- 函数名相同
- 函数参数不同(个数,类型,顺序满足其一即可)
注意:函数的返回值不能作为函数重载的条件。
构成重载的三种情况(满足以上一种的):
1.参数个数不同:
int add(int a, int b);
int add(int a);
**2.参数类型不同: **
int add(int a, int b);
int add(double a, double b);
**3.参数顺序不同: **
int add(int a, double b);
int add(double a, int b);
3.3.2函数重载得注意事项
- 引用作为重载条件
- 函数重载碰到默认参数
//引用作为重载条件
void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}
//调用有const修饰的引用和无const修饰的引用是不同的,可以实现重载
func(a); //调用无const
func(10);//调用有const
//函数重载中的默认参数
void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}
void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}
func2(10); //这样写以上两种func2函数都可以调用,这种写法容易造成歧义,不建议这样写
4、类和对象
C++面向对象有三大特性:封装、继承和多态。
C++认为万事万物皆为对象,对象上有其属性和行为。
例如:人可以作为对象,属性有姓名、身高、体重等;行为有走、跑、吃饭、睡觉。
车可以作为对象,属性有轮胎、方向盘、车灯等;行为有放音乐、开空调等。
4.1封装
4.1.1封装的意义
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装的意义一:
在设计类的时候,属性和行为写在一起,表现事物。
语法:class 类名{ 访问权限: 属性/行为};
示例:设计一个圆类,求圆的周长
示例代码:
//圆周率
const double PI = 3.14;
class Circle
{
public: //访问权限 公共的权限
//属性
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;
return 0;
}
封装的意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制。
访问权限有三种:
- public 公共权限 类内可以访问,类外也可以访问
- protected 保护权限 类内可以访问,类外不可以访问
- **private 私有权限 类内可以访问,类外不可以访问 **
注意:protected和private两种权限是不同的,主要体现在继承中。
4.1.2struct和class的区别
在C++中struct和class唯一的区别就是在于默认的访问权限不同
- struct默认权限为公共
- class默认权限为私有 class使用默认权限时类外不能访问
4.1.3成员属性设置为私有
优点:
- 将所有成员属性设置为私有,可以控制读写权限
- 对于写权限,可以检测数据的有效性
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; //只写 情人
};
4.2对象的初始化和清理
- 生活中我们买的电子产品都会有出厂设置,如果有一天我们不用的话可以对信息进行删除
- C++中的面向对象来源于生活,每个对象都会设置初始值最后进行销毁
4.2.1构造函数和析构函数
对象的初始化和清理是两个非常重要的安全问题,一个对象或者变量没有初始化状态的话,那么造成的后果是严重的。
同样的使用完一个对象或者变量,没有及时清理的话,也会造成安全问题。
C++利用构造函数和析构函数来解决以上问题,这两个函数由编译器自动调用,完成对象的初始化和清理工作;
我们不需要提供构造和析构函数,编译器会提供构造和析构函数的空实现。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名和类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象的时候自动进行构造,无需手动调用,而且只调用一次
析构函数的语法:~类名(){}
- 析构函数也是没有返回值的,也不写void
- 函数名和类名相同,在名称的前面加上~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前自动调用析构函数,无需手动调用,而且只调用一次
class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
4.2.2构造函数的分类和调用
两种分类方式:
按照参数可以分为:有参构造和无参构造
按照类型可以分为:普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
//2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();
//2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构
//2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//Person p5(p4);
4.2.3拷贝构造函数调用时机
C++中拷贝函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式但会局部对象
三种情况的代码实例:
//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);
}
//3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}
4.2.4构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无惨,函数体为空)
- 默认拷贝函数,对属性进行拷贝
构造函数的调用规则如下:
- 如果用户定义有参构造函数,C++不会提供默认构造函数,但是会提供拷贝函数
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
4.2.5深拷贝和浅拷贝
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
class Person {
public:
//无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int age ,int height) {
cout << "有参构造函数!" << endl;
m_age = age;
m_height = new int(height);
}
//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}
//析构函数
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
public:
int m_age;
int* m_height;
};
在类的拷贝函数中会存在一个问题:
进行拷贝之后,这两个对象指向的是同一块空间,而析构函数在每个对象销毁时都会执行一次,这就会造成堆区空间重复释放的问题,解决办法就是使用深拷贝,在堆区开辟不同的空间。
4.2.6初始化列表
作用:C++提供了初始化列表的语法,用来初始化属性。
语法:构造函数(): 属性1(值1), 属性2(值2)...{}
使用代码举个栗子:
class Person {
public:
//初始化列表方式初始化
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;
};
4.2.7类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
class A {}
class B
{
A a;
}
这样调用有一个顺序的问题,是先调用A的构造还是先调用B的构造,是先调用A的析构还是先调用B的析构?
class Phone
{
public:
Phone(string name)
{
m_PhoneName = name;
cout << "Phone构造" << endl;
}
~Phone()
{
cout << "Phone析构" << endl;
}
string m_PhoneName;
};
class Person
{
public:
//初始化列表可以告诉编译器调用哪一个构造函数
Person(string name, string pName) :m_Name(name), m_Phone(pName)
{
cout << "Person构造" << endl;
}
~Person()
{
cout << "Person析构" << endl;
}
void playGame()
{
cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
}
string m_Name;
Phone m_Phone;
};
void test01()
{
Person p("张三" , "苹果X");
p.playGame();
}
调用test01函数我们就会发现,初始化的时候先调用的是A的构造函数,然后再调用的B的构造函数;
进行销毁的时候,先调用的是B的析构函数,再调用的是A的析构函数。
这个循序我们可以把A比作一个汽车的零件,把B比作汽车,在组装的时候,肯定要先构造零件,再构造汽车;
在拆掉汽车的时候,要先把汽车拆掉,才能进一步拆除汽车的零件。
4.2.8静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称之为静态成员。
静态成员包括:
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内生命,类外初始化
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
类内生命,类外初始化:
class Person
{
public:
static int m_A; //静态成员变量
private:
static int m_B; //静态成员变量也是有访问权限的
};
int Person::m_A = 10;
int Person::m_B = 10;
静态成员变量在全局区,使用时使用对象和类名都能够访问到。
静态成员函数:
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;
}
};
静态成员函数只能访问静态成员变量;
4.3C++对象模型和this指针
4.3.1成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态的成员变量才属于类的对象上。
class Person {
public:
Person() {
mA = 0;
}
//非静态成员变量占对象空间
int mA;
//静态成员变量不占对象空间
static int mB;
//函数也不占对象空间,所有函数共享一个函数实例
void func() {
cout << "mA:" << this->mA << endl;
}
//静态成员函数也不占对象空间
static void sfunc() {
}
};
int main() {
cout << sizeof(Person) << endl;//只有非静态的成员变量属于对象,所以输出为4
return 0;
}
4.3.2this指针的概念
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用同一块代码;;
那么问题就是:这一块代码是如何区分是哪个对象调用了自己呢?
C++通过提供特殊的对象指针,this指针,解决了上述问题。
this指针指向被调用的成员函数所属的对象,this指针是隐含每一个非静态成员函数的一种指针。
this指针是不需要定义的,可以直接使用。
this指针的两个用途:
- 当形参和成员变量同名的时候,可以用this指针来区分(这个作用和Java中的this是相同的)
- 在类的非静态成员函数中返回对象本身,可以使用return *this
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p)
{
this->age += p.age;
return *this;
}
int age;
};
void test01()
{
Person p1(10);
cout << "p1.age = " << p1.age << endl;
Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout << "p2.age = " << p2.age << endl;
}
int main() {
test01();
return 0;
}
4.3.3空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是需要注意的是有没有用到this指针;
如果用到this指针,需要加以判断保证代码的健壮性。
//空指针访问成员函数
class Person {
public:
void ShowClassName() {
cout << "我是Person类!" << endl;
}
void ShowPerson() {
if (this == NULL) {
return;
}
cout << mAge << endl;
}
public:
int mAge;
};
void test01()
{
Person * p = NULL;
p->ShowClassName(); //空指针,可以调用成员函数
p->ShowPerson(); //但是如果成员函数中用到了this指针,就不可以了
}
int main() {
test01();
return 0;
}
成员函数在使用this指针的时候,需要进行检查。
4.3.4const修饰成员函数
常函数:
- 成员函数后面加const之后我们称这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable之后,在常函数中依然可以进行修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}
//this指针的本质是一个指针常量,指针的指向不可修改
//如果想让指针指向的值也不可以修改,需要声明常函数
void ShowPerson() const {
//const Type* const pointer;
//this = NULL; //不能修改指针的指向 Person* const this;
//this->mA = 100; //但是this指针指向的对象的数据是可以修改的
//const修饰成员函数,表示指针指向的内存空间的数据不能修改,除了mutable修饰的变量
this->m_B = 100;
}
void MyFunc() const {
//mA = 10000;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
//person.mA = 100; //常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量
//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数
}
int main() {
test01();
return 0;
}
本章完,后面会继续更新C++方面的内容,喜欢的家人们可以给个点赞,收藏+关注,谢谢大家!
版权归原作者 Catzzz666 所有, 如有侵权,请联系我们删除。