文章目录
- 前言
- C++的应用领域
- 核心编程
- 内存分区模型
- 1.程序运行前
- 2.程序运行后
- 3.new操作符
- 引用
- 函数
- 1.概述和函数原型
- 2.函数的定义和参数
- 3.使用函数处理不同类型的数据
- 4.微处理器如何处理函数调用
- 函数的分文件编写
- 指针和引用
- 什么是指针
- 动态内存分配
- 使用指针时常犯的编程错误
- 指针编程最佳实践
- 引用是什么
- 类和对象
- 封装
- struct和class区别
- 使用句点运算符访问成员
- 使用指针运算符(->)访问成员
- 对象的初始化和清理
- 构造函数和析构函数
- 重载构造函数
- 构造函数的分类及调用
- 拷贝构造函数调用时机
- 构造函数调用规则
- 深拷贝与浅拷贝
- 初始化列表
- 类对象作为类成员
- 静态成员
- C++对象模型和this指针
- 成员变量和成员函数分开存储
- this指针概念
- const类对象和const成员函数
- 总结
前言
C++在TIOBE上的流行指数一度逼近18%,可谓如日中天,所以毫无悬念,C++重夺年度语言…
目标:这系列文章会记录一下,编码风格,常见错误,如何调试,好的编程实战,如何测试等学习内容
C++的应用领域
- 服务器端开发:很多游戏或者互联网公司的后台服务器程序都是基于C++开发的,而且大部分是linux操作系统
- 嵌入式开发:嵌入式相关的岗位需求比较多,主要是硬件产品的驱动开发,很多大公司,比如华为、小米、vivo和一些芯片公司都在大量的招聘嵌入式开发工程师.
- 游戏 :以腾讯微代表的游戏公司,很多游戏都是C++开发的.
- 人工智能:深度学习工程化开发。人工智能目前可以落地的主要领域,是安防和自动驾驶,这块都会使用C++开发,包括深度学习算法SDK,深度学习框架开发等。
- 数字图像处理:比如像AutoCAD的系统开发,像OpenCV的视觉识别等等。
软件开发,通常遵循如下流程:
核心编程
内存分区模型
程序在执行时,将内存大方向划分为4个区域:
- 代码区:存放函数体的二进制代码,由操作系统进行管理
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,如果程序员不释放,程序结束时由操作系统回收
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程
1.程序运行前
未执行该程序前分为两个区域
-
代码区:
1️⃣存放 CPU 执行的机器指令。
2️⃣代码区是共享的,共享的目标是对于频繁被执行的程序,只需要在内存中有一份代码即可。
3️⃣代码区是只读的。使其只读的原因是防止程序意外地修改了它的指令。 -
全局区:
1️⃣全局变量和静态变量存放在此。
2️⃣ 全局区包含常量区,字符串常量和其他常量存放
3️⃣该区域的数据在程序结束后由操作系统释放
//全局变量
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");
return 0;
}
2.程序运行后
-
栈区:
由编译器自动分配释放, 存放函数的参数值,局部变量等
不会返回局部变量的地址,栈区开辟的数据由编译其自动释放
int * func()
{
int a =10;
return &a;
}
- 堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收,在C++中主要利用new在堆区开辟内存
3.new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
语法: new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
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];
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;
}
引用
作用:给变量起别名
语法: 数据类型 &别名 = 原名
- 引用必须初始化
- 引用在初始化后,不可以改变
int main() {
int a = 10;
int b = 20;
//int &c; //错误,引用必须初始化
int &c = a; //一旦初始化后,就不可以更改
c = b; //这是赋值操作,不是更改引用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
system("pause");
return 0;
}
- 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参
优点:可以简化指针修改实参
//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& 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;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
system("pause");
return 0;
}
- 引用的本质
本质:引用的本质在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;
}
- 常量引用
作用:常量引用主要用来修饰形参,防止误操作
在函数形参列表中,可以加const修饰形参,防止形参改变实参
//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
//v += 10;
cout << v << endl;
}
int main() {
//int& ref = 10; 引用本身需要一个合法的内存空间,因此这行错误
//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;
//ref = 100; //加入const后不可以修改变量
cout << ref << endl;
//函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);
system("pause");
return 0;
}
函数
函数让您能够划分和组织程序的执行逻辑。通过使用函数,可将应用程序的内容划分成依次调用的逻辑块。
函数是子程序,可接受参数并返回值,要让函数执行其任务,必须调用它。
- 为何需要编写函数;
- 函数原型和函数定义;
- 给函数传递参数以及从函数返回值;
- 重载函数;
- 递归函数;
- C++11 lambda 函数。
1.概述和函数原型
作用:将一段经常使用的代码封装起来,减少重复代码
一个较大的程序,一般分为若干个程序块,每个模块实现特定的功能。
cout << "Area is: " << Area(radius) << endl;
cout << "Circumference is: " << Circumference(radius) << endl;
函数原型:就是指出函数的名称(Area)、函数接收的参数列表(double radius)以及返回值的类型(double)
函数原型告诉编译器,Area 和 Circumference 是函数,它们接受一个类型为 double 的参数,并返回一个类型为 double 的值
2.函数的定义和参数
函数的定义一般主要有5个步骤:
1、返回值类型
2、函数名
3、参数表列
4、函数体语句
5、return 表达式
语法:
返回值类型 函数名 (参数列表)
{
函数体语句
return表达式
}
- 返回值类型 :一个函数可以返回一个值。在函数定义中
- 函数名:给函数起个名称
- 参数列表:使用该函数时,传入的数据
- 函数体语句:花括号内的代码,函数内需要执行的语句
- return表达式: 和返回值类型挂钩,函数执行完后,返回相应的数据
示例:定义一个加法函数,实现两个数相加
//函数定义
int add(int num1, int num2)
{
int sum = num1 + num2;
return sum;
}
参数
- 编写没有参数和返回值的函数
- 带默认值的函数参数
3.使用函数处理不同类型的数据
您可创建两个名称和返回类型相同,但参数不同的函数。您可创建这样的函数,即其参数不是在函数内部创建和销毁的,为此可使用在函数退出后还可用的引用,这样可在函数中操纵更多数据或参数
- 函数重载
名称和返回类型相同,但参数不同的函数成为重载函数。
double Area(double radius); // for circle
double Area(double radius, double height); // for cylinder
这两个函数同名,都叫 Area(),返回类型相同,都是 double,但参数不同,因此它们是重载的。
- 将值传递给函数
– 所谓值传递,就是函数调用时实参将数值传入给形参
– 值传递时,如果形参发生,并不会影响实参
void swap(int num1, int num2)
{
cout << "交换前:" << endl;
cout << "num1 = " << num1 << endl;
cout << "num2 = " << num2 << endl;
int temp = num1;
num1 = num2;
num2 = temp;
cout << "交换后:" << endl;
cout << "num1 = " << num1 << endl;
cout << "num2 = " << num2 << endl;
//return ; 当函数声明时候,不需要返回值,可以不写return
}
int main() {
int a = 10;
int b = 20;
swap(a, b);
cout << "mian中的 a = " << a << endl;
cout << "mian中的 b = " << b << endl;
system("pause");
return 0;
}
- 按引用传递参数
void Area(double radius, double& result)
{
result = Pi * radius * radius;
}
double areaFetched = 0;
Area(radius, areaFetched);
cout << "The area is: " << areaFetched << endl;
别遗漏了第二个形参 result 旁边的&,它告诉编译器,不要将
第二个实参复制给函数,而将指向该实参的引用传递给函数。返回类型变成了 void,因为该函数不再通过返回值提供计算得到的面积,而按引用以输出参数的方式提供它
4.微处理器如何处理函数调用
在微处理器级,函数调用是如何实现的呢?虽然确切地了解这一点不是非常重要,但您可能发现它很有趣。了解这一点有助于明白 C++为何支持本节后面将介绍的内联函数。
函数调用意味着微处理器跳转到属于被调用函数的下一条指令初执行。函数执行完的指令后,返回到最初离开的位置。
实现这个逻辑就是
编译器
将函数调用转换成一条供微处理器执行的CALL指令
- 该指令指出了接下来要获取的
指令所在的地址
,该地址归函数所有 - 遇到 CALL 指令时,微处理器将调用函数后将要
执行的指令的位置保存到栈中
- 再跳转到 CALL 指令包含的内存单元处
- 内联函数
常规函数调用被转换为CALL指令,这会导致栈操作,微处理器跳转到函数执行栈。
double GetPi()
{
return 3.14159;
}
相对于实际执行 GetPi( )所需的时间,执行函数调用的开销可能非常高。这就是 C++编译器允许程序员将这样的函数声明为内联的原因。程序员使用关键字 inline 发出请求,要求在函数被调用时就地展开它们
:
inline double GetPi()
{
return 3.14159;
}
inline通常被编译器视为请求,将函数的内容直接放到调用它的地方,以提高代码的执行速度。
但是,将函数声明为内联的会导致代码急剧膨胀,在声明为内联的函数做大量复杂处理。尽可能少用关键字inline,仅当函数非常简单,需要减低开销,才用其关键字。
- 自动推断返回类型
关键字 auto,使用它可让编译器根据赋给变量的初始值来推断变量的类型。从 C++14起,这种做法也适用于函数:使用 auto 让编译器根据您返回的值来推断函数的返回类型,而不直接指定返回类型。
auto Area(double radius){
return pi*radius*radius;
}
int main()
{
cout << "Enter radius: ";
double radius = 10;
cout << "Area is: " << Area(radius) << endl;
return 0;
}
编译器将根据 return 语句中使用 double变量的表达式来推断返回类型
- lambda 函数(有些难理解,先略过)
函数的分文件编写
作用:让代码结构更加清晰
函数分文件编写一般有4个步骤:
- 创建后缀名为.h的头文件
- 创建后缀名为.cpp的源文件
- 在头文件中写函数的声明
- 在源文件中写函数的定义
swap.h文件
#include<iostream>
using namespace std;
//实现两个数字交换的函数声明
void swap(int a, int b);
swap.cpp文件
#include "swap.h"
void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
main函数文件
#include "swap.h"
int main() {
int a = 100;
int b = 200;
swap(a, b);
system("pause");
return 0;
}
wuth
指针和引用
事实上,C++让您能够在字节和比特级调整应用程序的性能,编写高效地利用系统资源的程序,理解指针和引用是必不可少的一步。
- 什么是指针;
- 什么是自由存储区;
- 如何使用运算符new和delete分配和释放内存;
- 如何使用指针和动态分配编写稳定的应用程序;
- 什么是引用;
- 指针和引用的区别;
- 什么情况下使用指针,什么情况下使用引用。
什么是指针
指针是存储内存地址的变量。就像是int变量用于存储整数值一样,指针变量用于存储内存地址。
指针是一个变量,所以变量一样,执政也占用内存空间(其地址为 0x101),指针包含的值(这里为 0x558)被解读为内存地址
显示十六进制数时,通常使 用前缀 0x
- 声明指针
变量声明一样,指针也需要声明,int指针包含的地址对应的内存单元存储另一个整数,指向特定的类型。
指针变量定义语法:
数据类型 * 变量名
;
PointedType * PointerVariableName;
大多数变量声明一样,除非指针进行初始化,否则它包含的值将是随机的,因此将指针初始化为NULL,是个可检查的值,且不会是内存;如int指针:
int *pointsToInt = NULL;
❌与您学过的所有数据类型一样,除非对指针进行初始化,否则它包含的将是垃圾值。对指针来说,这种垃圾值非常危险,因为指针包含的值被视为地址。未初始化的指针可能导致程序访问非法内存单元,进而导致程序崩溃。
- 使用引用运算符(&)获取变量的地址
如果 varName 是一个变量,&varName
将是存储该变量的内存的地址。
int age = 30;
&age是存储该变量的值的内存地址
- 使用指针存储地址
int main() {
//1、指针的定义
int a = 10; //定义整型变量a
//指针定义语法: 数据类型 * 变量名 ;
int * p;
//指针变量赋值
p = &a; //指针指向变量a的地址
cout << &a << endl; //打印数据a的地址
cout << p << endl; //打印指针变量p
//2、指针的使用
//通过*操作指针变量指向的内存
cout << "*p = " << *p << endl;
system("pause");
return 0;
}
指针变量和普通变量的区别
普通变量存放的是数据, 指针变量存放的是地址
指针变量可以通过*操作符
,操作指针变量指向的内存空间,这个过程称为解引用
1. 总结1: 我们可以通过 & 符号 获取变量的地址
2. 总结2:利用指针可以记录地址
3. 总结3:对指针变量解引用,可以操作指针指向的内存
- 将sizeof( )用于指针的结果
指针是包含内存地址的变量。因此无论指针指向哪种类型的变量,其内容都是一个地址—一个数字。在特定的系统中,存储地址所需的字节数是固定的。因此,将 sizeof( )用于指针时,结果取决于编译程序时使用的编译器和针对的操作系统,与指针指向的变量类型无关。
int main() {
int a = 10;
int * p;
p = &a; //指针指向数据a的地址
cout << *p << endl; //* 解引用
cout << sizeof(p) << endl;
cout << sizeof(char *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(double *) << endl;
system("pause");
return 0;
}
总结:所有指针类型在32位操作系统下是4个字节
❌将 sizeof 用于指针的结果为 4 字节
,但在您的系统上结果可能不同。这里的输出是使用 32 位编译器编译代码时得到的,如果您使用的是 64 位编译器,并在 64 位系统上运行该程序,可能发现将 sizeof 用于指针的结果为 64 位,即 8字节
。
动态内存分配
int myNums[100];
程序将存在两个问题。
1.这限制了程序的容量,无法存储 100 个以上的数字。
2.如果只需存储 1 个数字,却为 100 个数字预留存储空间,这将降低系统的性能。
为帮助您更好地管理应用程序占用的内存,C++提供了两个运算符—new
和 delete
- new
使用new来分配新的内存块。通常情况下,如果成功,new将返回指向一个指针,指向分配的内存,否则将引起异常。使用new时,需要指定要为那种数据类型分配内存。
Type* Pointer = new Type;
需要为多个元素分配内存时
Type* Pointer = new Type[numElements];
因此,如果需要给整型分配内存,可使用如下语法:
int* pointToAnInt = new int; // 得到整型的指针
int* pointToNums = new int[10]; // 指向一块10个整型的指针
- delete
不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给您的应用程序。这将减少可供其他应用程序使用的系统内存量,甚至降低您的应用程序的执行速度。这被称为内存泄露
,应不惜一切代价避免这种情况发生。
int* pointsToAnAge = new int;
cout << "Age " << *pointsToAnAge << " is stored at 0x" << hex <<
pointsToAnAge << endl; //Age 9 is stored at 0x00338120
delete pointsToAnAge; // release memory
- 指针和数组
作用:利用指针访问数组的元素
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int * p = arr; //指向数组的指针
cout << "第一个元素: " << arr[0] << endl;
cout << "指针访问第一个元素: " << *p << endl;
for (int i = 0; i < 10; i++)
{
//利用指针遍历数组
cout << *p << endl;
p++;
}
system("pause");
return 0;
}
- 指针和函数
作用:利用指针作函数参数,可以修改实参的值
//值传递
void swap1(int a ,int b)
{
int temp = a;
a = b;
b = temp;
}
//地址传递
void swap2(int * p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main() {
int a = 10;
int b = 20;
swap1(a, b); // 值传递不会改变实参
swap2(&a, &b); //地址传递会改变实参
cout << "a = " << a << endl;
cout << "b = " << b << endl;
system("pause");
return 0;
}
- const修饰指针
const声明的变量,可确保变量的取值在整个生命周期内都固定为起始值。变量不可以修改。
指针也是变量,指针是特殊变量,包含内存地址,还用于修改内存中的数据块。
const修饰指针有三种情况
-
指针包含的地址是常量,不能修改。但可以修改指针指向的数据:const修饰指针 — 常量指针
-
指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方:const修饰常量 — 指针常量
-
指针包含的地址以及它指向的值都是常量,不能修改(这种组合最严格): const即修饰指针,又修饰常量
int main() {
int a = 10;
int b = 10;
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
//const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误
system("pause");
return 0;
}
使用指针时常犯的编程错误
不同于 C#和 Java 等基于运行时环境的新语言,C++没有自动垃圾收集器对程序已分配但不能使用的内存进行清理。使用指针来管理内
存资源时,程序员很容易犯错。
- 内存泄露
运行时间越长,占用的内存越多,系统越慢。如果在使用 new 动态分配的内存不再需要后,程序员没有使用配套的 delete 释放,通常就会出现这种情况。
int* pointToNums = new int[5];
// 忘记使用删除点来释放;
pointToNums = new int[10];//泄漏先前分配的内存
- 指针指向无效的内存单元、空指针和野指针
无效指针
:使用运算符 * 对指针解除引用,以访问指向的值时,务必确保指针指向有效的内存单元,否则程序就要崩溃。但一个非常常见的导致应用程序崩溃的原因就是无效指针。
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: // uninitialized pointer (bad)
6: bool* isSunny;
7:
8: cout << "Is it sunny (y/n)? ";
9: char userInput = 'y';
10: cin >> userInput;
11:
12: if (userInput == 'y')
13: {
14: isSunny = new bool;
15: *isSunny = true;
16: }
17:
18: // isSunny contains invalid value if user entered 'n'
19: cout << "Boolean flag sunny says: " << *isSunny << endl;
20:
21: // delete being invoked also when new wasn't
22: delete isSunny;
23:
24: return 0;
25: }
输出:
Is it sunny (y/n)? y
Boolean flag sunny says: 1
再次运行的输出:
Is it sunny (y/n)? n
<CRASH!>
第一次运行:y;程序执行,因此isSunny使用 new 分配这个指针
第二次运行:n;程序崩溃,因为isSunny包含无效的内存地址,对这个无效的指针解除引用,导致应用程序崩溃。
第 22 行对这个指针调用 delete,但并未使用 new 分配这个指针,这也是大错特错。如果有指针的多个拷贝,只需对其中一个调用 delete
空指针
:指针变量指向内存中编号为0的空间
用途:初始化指针变量
注意:空指针指向的内存是不可以访问的
int main() {
//指针变量p指向内存地址编号为0的空间
int * p = NULL;
//访问空指针报错
//内存编号0 ~255为系统占用内存,不允许用户访问
cout << *p << endl;
system("pause");
return 0;
}
野指针
:指针变量指向非法的内存空间
int main() {
//指针变量p指向内存地址编号为0x1100的空间
int * p = (int *)0x1100;
//访问野指针报错
cout << *p << endl;
system("pause");
return 0;
}
- 检查使用 new 发出的分配请求是否得到满足
在前面的代码中,我们都假定 new 将返回一个指向内存块的有效指针。事实上,除非请求分配的内存量特大,或系统处于临界状态,可供使用的内存很少,否则 new 一般都能成功。有些应用程序需要请求分配大块的内存(如数据库应用程序),因此最好不要假定内存分配能够成功。C++提供了两种确认指针有效的方法,默认方法是使用异常(这也是前面一直使用的方法),即如果内存分配失败,将引发 std::bad_alloc 异常。这导致应用程序中断执行,除非您提供了异常处理程序,否则应用程序将崩溃,并显示一条类似于“异常未处理”的消息。
指针编程最佳实践
⭕️
1️⃣务必初始化指针变量,否则它将包含垃圾值。
这些垃圾值被解读为地址,但您的应用程序并未获
得访问这些地方的授权。如果不能将指针初始化为
new 返回的有效地址,可将其初始化为 NULL。
2️⃣务必仅在指针有效时才使用它,否则程序可能
崩溃。
3️⃣对于使用 new 分配的内存,一定要记得使用
delete 进行释放,否则应用程序将泄露内存,进而降
低系统的性能。
❌
1️⃣使用 delete 释放内存块或指针后,不要访问它。
2️⃣不要对同一个内存地址调用 delete 多次。
3️⃣使用完动态分配的内存块后,别忘了对其调用
delete,以免泄露内存。
引用是什么
引用是变量的别名、要声明引用,可使用引用运算符(&);
VarType original = Value;
VarType& ReferenceVariable = original;
深入了解如何声明和使用引用。
0: #include <iostream>
1: using namespace std;
2:
3: int main()
4: {
5: int original = 30;
6: cout << "original = " << original << endl;
7: cout << "original is at address: " << hex << &original << endl;
8:
9: int& ref1 = original;
10: cout << "ref1 is at address: " << hex << &ref1 << endl;
11:
12: int& ref2 = ref1;
13: cout << "ref2 is at address: " << hex << &ref2 << endl;
14: cout << "Therefore, ref2 = " << dec << ref2 << endl;
15:
16: return 0;
17: }
输出:
original = 30
original is at address: 0099F764
ref1 is at address: 0099F764
ref2 is at address: 0099F764
Therefore, ref2 = 30
类和对象
至此,您探索了简单的程序。这种程序从 main( )开始执行,包含局部变量、全局变量和常量,并将执行逻辑划分为可接受参数和返回值的函数。前面使用的都是过程性编程风格,未涉及面向对象。换句话说,您需要学习 C++面向对象编程基本知识。
在本章中,您将学习:
- 什么是类和对象;
- 类如何帮助您整合数据和处理数据的函数;
- 构造函数、复制构造函数和析构函数;
- 移动构造函数是什么;
- 封装和抽象等面向对象的概念;
- this 指针;
- 结构是什么,它与类有何不同。
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
封装
封装是C++面向对象三大特性之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装意义一:
在设计类的时候,属性和行为写在一起,表现事物
语法: class 类名{ 访问权限: 属性 / 行为 };
示例1:设计一个圆类,求圆的周长
//圆周率
const double PI = 3.14;
//1、封装的意义
//将属性和行为作为一个整体,用来表现生活中的事物
//封装一个圆类,求圆的周长
//class代表设计一个类,后面跟着的是类名
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;
system("pause");
return 0;
}
封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
- public 公共权限
- protected 保护权限
- private 私有权限
//三种权限
//公共权限 public 类内可以访问 类外可以访问
//保护权限 protected 类内可以访问 类外不可以访问
//私有权限 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;
}
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;
}
使用句点运算符访问成员
firstMan 是 Human 类的对象,是这个类存在于现实世界(运行阶段)的化身:
Human firstMan; // 一个实例,即人类的对象
类声明表明,firstMan 有 dateOfBirth 等属性,可使用句点运算符(.)
来访问:
firstMan.dateOfBirth = "1970";
- 如果有一个
指针 firstWoman
,它指向 Human 类的一个实例,则可使用指针运算符(->)
来访问成员(这将在下一小节介绍),
Human* firstWoman = new Human();
firstWoman->IntroduceSelf();
- 也可使用
间接运算符(*)来获取对象
,再使用句点运算符来访问成员:
Human* firstWoman = new Human();
(*firstWoman).IntroduceSelf();
使用指针运算符(->)访问成员
如果对象是使用 new 在自由存储区中实例化的,或者有指向对象的指针,则可使用指针运算符(->)来访问成员属性和方法:
Human* firstWoman = new Human();
firstWoman->dateOfBirth = "1970";
firstWoman->IntroduceSelf();
delete firstWoman;
对象的初始化和清理
- 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
- C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
编译器提供的构造函数和析构函数是空实现。
- 构造函数是一种特殊的函数(方法),在根据类创建对象时被调用。与函数一样,构造函数也可以
重载。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
class Person
{
public:
//构造函数
Person()
{
cout << "Person的构造函数调用" << endl;
}
}
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
class Person
{
public:
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
重载构造函数
与函数一样,构造函数也可重载,因此可创建一个将姓名作为参数的构造函数,如下所示
class Human
{
public:
Human()
{
// default constructor code here
}
Human(string humansName)
{
// overloaded constructor code here
}
};
构造函数的分类及调用
两种分类方式:
- 按参数分为: 有参构造和无参构造
- 按类型分为: 普通构造和拷贝构造
三种调用方式:
1️⃣括号法
2️⃣显示法
3️⃣隐式转换法
/1、构造函数分类
// 按照参数分类分为 有参和无参构造 无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造
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;
};
//2、构造函数的调用
//调用无参构造函数
void test01() {
Person p; //调用无参构造函数
}
void test02(){
//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);
}
int main() {
test01();
//test02();
system("pause");
return 0;
}
拷贝构造函数调用时机
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);
}
//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;
}
构造函数调用规则
默认情况下,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;
}
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
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;
};
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;
}
初始化列表
作用:
C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性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;
}
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
class A {}
class B
{
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();
}
int main() {
test01();
system("pause");
return 0;
}
总结:
B->A,
构造的顺序是 :先调用对象成员的构造,再调用本类构造。
A->B
析构顺序与构造相反
静态成员
静态成员就是在成员变量
和成员函数
前加上关键字static
,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
静态成员变量
class Person
{
public:
static int m_A; //静态成员变量
//静态成员变量特点:
//1 在编译阶段分配内存
//2 类内声明,类外初始化
//3 所有对象共享同一份数据
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;
}
静态成员函数
class Person
{
public:
//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量
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;
}
C++对象模型和this指针
在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;
system("pause");
return 0;
}
成员变量和成员函数分开存储
类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
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;
system("pause");
return 0;
}
this指针概念
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。
c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
- this指针是隐含每一个非静态成员函数内的一种指针
- this指针不需要定义,直接使用即可
this指针的用途:
- 当形参和成员变量同名时,用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();
system("pause");
return 0;
}
const类对象和const成员函数
常函数:
- 成员函数后加
const
后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable
后,在常函数中依然可以修改
class Person {
public:
Person() {
m_A = 0;
m_B = 0;
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
常对象:
- 声明对象前加const称该对象为
常对象
- 常对象只能调用
常函数
class Person {
public:
//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;// 编译失败: const 成员函数不能修改成员变量
}
public:
int m_A;
mutable int m_B; //可修改 可变的
};
//const修饰对象 常对象
void test01() {
const Person person; //常量对象
cout << person.m_A << endl;
person.mA = 100; // 编译失败: 不能修改const 对象的成员变量,常对象不能修改成员变量的值,但是可以访问
person.m_B = 100; //但是常对象可以修改mutable修饰成员变量
//常对象访问成员函数
person.MyFunc(); //常对象不能调用const的函数,这将导致编译器错误。
}
int main() {
test01();
system("pause");
return 0;
}
- 非常量对象上调用const成员函数
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() const // const
{
std::cout << year << '/' << month << '/' << day;
}
};
int main()
{
Date today { 2020, 10, 14 }; // 非常对象
today.print(); // ok: 可以调用
return 0;
}
由于const成员函数可以在常量和非常量对象上调用,因此如果成员函数不修改对象的状态,则应将其设置为常量。
- 常量引用传递的常量对象
实例化常量局部变量创建常量对象的一种方法,但获取常量对象的更常见方法时通过常量引用将对象传递给函数。
#include <iostream>
struct Date
{
int year {};
int month {};
int day {};
void print() // non-const 修复:将 print() 设置为 const:
{
std::cout << year << '/' << month << '/' << day;
}
};
void doSomething(const Date &date)
{
date.print();
}
int main()
{
Date today { 2020, 10, 14 }; // non-const
today.print();
doSomething(today);
return 0;
}
在doSomething()函数内部,date被视为常量对象(因为它是通过常量引用传递的)。对于该常量,调用非const成员函数print()。由于不能对常量对象调用非const成员函数,这将导致编译错误。
总结
内容比较多,接下来继续分享包括重点内容,包括重载,继承,多态,智能指针,宏和模板,标准库等内容❤️