目录
1、内存分区模型
1.1程序运行前
1.2程序运行后
2、引用
2.1引用的基本类型
2.2引用的注意事项
2.3引用作为函数参数
2.4引用作为函数的返回值
2.5引用的本质
2.6常量的引用
3、函数提高
3.1函数默认的参数
3.2函数占位参数
3.3函数重载
3.3.1函数重载的概述
3.3.2函数重载得注意事项
4、类和对象
4.1封装
4.1.1封装的意义
4.1.2struct和class的区别
4.1.3成员属性设置为私有
4.2对象的初始化和清理
4.2.1构造函数和析构函数
4.2.2构造函数的分类和调用
4.2.3拷贝构造函数调用时机
4.2.4构造函数调用规则
4.2.5深拷贝和浅拷贝
4.2.6初始化列表
4.2.7类对象作为类成员
4.2.8静态成员
4.3C++对象模型和this指针
4.3.1成员变量和成员函数分开存储
4.3.2this指针的概念
4.3.3空指针访问成员函数
4.3.4const修饰成员函数
C++在程序执行时, 内存可分为4个区域:
内存四区的意义: 内存的四个区域,生命周期各不相同,让我们的编程可以更灵活。
四个区域主要可以体现在程序运行前和程序运行后:
在程序编译后,生成exe可执行程序,未执行该程序前可以分为两个区域:
代码区:存放CPU执行的机器指令。
代码区的两个特点:
全局区:全局变量和静态变量存放在该区域。
全局区还包含了常量区,字符串常量和其他常量同样存放在全局区,该区域的数据在程序结束后由操作系统释放。
其他数据类型是否在全局区可以使用代码进行测试,像下面一段代码是测量常量字符串、const修饰的全局变量、全局变量、static修饰的静态变量是否在同一区域:
#include
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
从这个运行结果其实就能够直观的看出这些数据在同一个区域,因为他们的内存编号转化为十进制之后相差不大;当然,其他区域的数据是否也在全局区或者其他区域这个可以自行测量。
栈区:由编译器自行分配和释放,存放函数的参数,局部变量等
使用栈区时的注意事项:不要返回局部变量的地址(否则会造成野指针问题),栈区数据的开辟由编译器自动释放。
分析一下下面这段程序,这段程序有什么错误?
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;
}
作用:给变量起别名
语法:数据类型 &别名 = 原名
#include
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的值也随之改变。
注意:在使用引用的时候,一定不能写成这样
int& b;
b = a;
第一,引用在使用的时候必须进行初始化;
第二,b = a,进行的是赋值操作;
作用:函数传参的时候,可以利用引用的技术让形参修饰实参
有点:可以简化指针修改实参
下面是分别使用值传递、址传递、引用传递进行交换的实例:
//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;
}
从址传递和引用传递不难看出这两种的作用效果是相同的,但是引用的代码更简洁一点。
作用:引用可以作为函数的返回值存在。
注意:一定不要返回局部变量的引用 函数调用可以作为左值
//返回静态变量引用
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;
}
函数调用作为左值的时候相当于一个数的别名,同样可以对此数据进行改变。
本质:引用的本质在C++内部是实线是一个指针常量。
我们之前所说的指针变量运用的主要场景是 数据类型* p = &a;
引用的本质其实也是指针,只不过这个指针是一个常量,其中的数据是不能更改的;
也就是const 数据类型 *p = &a;
作用:常量的引用主要是限制数据只是读的,防止对数据进行误操作。
在引用数据的前面加const修饰
//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
//v += 10;
cout << v << endl;
}
//修饰形参,使形参的内容不可以改变
在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;
}
C++中函数的形参列表里面可以有占位参数,用来做占位,调用函数的时候必须填补占位参数的位置。
语法: 返回值类型 函数名(数据类型){}
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "func函数的调用" << endl;
}
int main() {
func(10,10); //占位参数必须填补
return 0;
}
函数重载:函数名可以相同,提高复用性
函数重载需要满足的条件:
注意:函数的返回值不能作为函数重载的条件。
构成重载的三种情况(满足以上一种的):
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);
//引用作为重载条件
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函数都可以调用,这种写法容易造成歧义,不建议这样写
C++面向对象有三大特性:封装、继承和多态。
C++认为万事万物皆为对象,对象上有其属性和行为。
例如:人可以作为对象,属性有姓名、身高、体重等;行为有走、跑、吃饭、睡觉。
车可以作为对象,属性有轮胎、方向盘、车灯等;行为有放音乐、开空调等。
封装的意义一:
在设计类的时候,属性和行为写在一起,表现事物。
语法: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两种权限是不同的,主要体现在继承中。
在C++中struct和class唯一的区别就是在于默认的访问权限不同
优点:
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; //只写 情人
};
对象的初始化和清理是两个非常重要的安全问题,一个对象或者变量没有初始化状态的话,那么造成的后果是严重的。
同样的使用完一个对象或者变量,没有及时清理的话,也会造成安全问题。
C++利用构造函数和析构函数来解决以上问题,这两个函数由编译器自动调用,完成对象的初始化和清理工作;
我们不需要提供构造和析构函数,编译器会提供构造和析构函数的空实现。
构造函数语法:类名(){}
析构函数的语法:~类名(){}
class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
两种分类方式:
按照参数可以分为:有参构造和无参构造
按照类型可以分为:普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
//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);
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;
}
默认情况下,C++编译器至少给一个类添加3个函数
构造函数的调用规则如下:
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;
};
在类的拷贝函数中会存在一个问题:
进行拷贝之后,这两个对象指向的是同一块空间,而析构函数在每个对象销毁时都会执行一次,这就会造成堆区空间重复释放的问题,解决办法就是使用深拷贝,在堆区开辟不同的空间。
作用: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;
};
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比作汽车,在组装的时候,肯定要先构造零件,再构造汽车;
在拆掉汽车的时候,要先把汽车拆掉,才能进一步拆除汽车的零件。
静态成员就是在成员变量和成员函数前面加上关键字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;
}
};
静态成员函数只能访问静态成员变量;
在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;
}
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用同一块代码;;
那么问题就是:这一块代码是如何区分是哪个对象调用了自己呢?
C++通过提供特殊的对象指针,this指针,解决了上述问题。
this指针指向被调用的成员函数所属的对象,this指针是隐含每一个非静态成员函数的一种指针。
this指针是不需要定义的,可以直接使用。
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;
}
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指针的时候,需要进行检查。
常函数:
常对象:
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++方面的内容,喜欢的家人们可以给个点赞,收藏+关注,谢谢大家!