目录
C++在程序执行时, 内存可分为4个区域:
内存四区的意义: 内存的四个区域,生命周期各不相同,让我们的编程可以更灵活。
四个区域主要可以体现在程序运行前和程序运行后:
在程序编译后,生成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
从这个运行结果其实就能够直观的看出这些数据在同一个区域,因为他们的内存编号转化为十进制之后相差不大;当然,其他区域的数据是否也在全局区或者其他区域这个可以自行测量。
栈区:由编译器自行分配和释放,存放函数的参数,局部变量等
使用栈区时的注意事项:不要返回局部变量的地址(否则会造成野指针问题),栈区数据的开辟由编译器自动释放。
分析一下下面这段程序,这段程序有什么错误?
- 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 <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的值也随之改变。
- 注意:在使用引用的时候,一定不能写成这样
- 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++方面的内容,喜欢的家人们可以给个点赞,收藏+关注,谢谢大家!