目录
一.C++中的结构体
1.c++中的结构体相当于java中的类,可以存放多个变量,如下:
#include <stdio.h>
#include <iostream>
struct Student
{
int math;
int english;
};
int main(int argc,char **argv)
{
struct Student stu = {95, 93};
std::cout << "math: " << stu.math << ", english: " << stu.english << std::endl;
return 0;
}
二.C++中的指针
-
指针变量:存放变量在内存中的地址。定义一个指针后,它里面存放的是随机地址,此时它是野指针,操作野指针非常危险,所以要给指针赋值。
-
取地址符:&
例如:int a=10;
int *p; //这个写法就属于野指针,野指针的意思是随机指向任意的位置,正确做法是将指针赋值为空int*p=nullptr,空指针意思是这个指针不指向任何地方。
p=&a; //此时指针变量p里存放的就是变量a的地址
-
指针运算符:*(用来取得某个地址上的数据)
三.malloc与free
1.自动变量存储在栈中,无需手动释放内存,在作用域结束时自动释放内存
2.在堆区手动开辟内存分配空间用malloc,malloc返回值是一个指针,指向分配出来的首地址,堆区的内存在作用域结束后不会自动销毁,需要手动释放,用free。
此处需要注意free()方法释放内存后p指针也需要赋值为NULL,不然也是野指针,还可以访问到原先的内存位置,并且操作原先内存位置的内容,而此时已经释放内存,系统是认为此处的内存位置是可以使用的,安全的,但是现在如果p指针可以访问并操作原先内存的位置,则是不安全的,会很小概率产生问题,但是产生了问题就很难定位。
四.数组与指针
数组名就是指针,也是数组中第一个元素的地址
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char **argv)
{
int * p = (int *)malloc(5 * sizeof(int));//分配5个int大小的存储空间
//*(p+2)相当于p[2]
free(p);
return 0;
}
五.值传递与地址传递原理
以上是值传递的概念,单纯地将a的值进行调用,change函数里的a变成了4,但是main函数里面的还是5,因为change函数执行完毕就销毁了里面的变量,所以其实change和main里的a完全是两个东西。
这里进行了址传递,因为函数本质是将a的地址传递到了change函数里,所以改变的是a地址里的数据,这样主函数和调用函数的内容就一致了。
六.C++中的内联函数
关键字:inline,将此关键字加在函数定义的位置
内联函数相当于将函数中的代码拷贝到调用函数的位置,减少了调用函数的内存消耗
示例:
inline int add(int a, int b)
{
return a + b;
}
七.C++中的位运算符
什么是位运算?
C++ 中有一些基本数据类型例如 int,short,char
最小的就是 char 了,没有比一个字节更小的了,那么我们想要做颗粒度更细的操作的时候,应该怎么办呢?
这时候就要用到位运算了。
& 与运算
参加运算的两个数据,按二进制位进行与运算。如果两个相应的二进制位都为1,则该位的结果值为1,否则为0。
10100011 & 00100101 ------------ 00100001
| 或运算
两个相应的二进制位中只要有一个为1,该位的结果值为1。
00110000 | 00000111 ------------ 00110111
^ 异或
若参加运算的两个二进制位值相同则为0,否则为1。
00111001 ^ 00101010 ------------ 00010011
~ 取反
这是一元运算符,用于求整数的二进制反码,即分别将操作数各二进制位上的 1 变为 0,0 变为 1。
例如, 00001001 的二进制数取反,就变成了 11110110
<< 左移
各位全部左移若干位,高位丢弃,低位补 0 。
>> 右移
各二进位全部右移若干位,对无符号数,高位补 0 ,有符号数,各编译器处理方法不一样,有的补符号位,有的补 0 。
七.一起探索指针的本质
1.指针的本质:可以理解为一个箭头指向了一个内存地址,或者说一个绳子牵着一个内存地址(*:解引用、&:取地址)
2.*与&在不同位置的含义。
*在不同地方的含义
①、在定义指针中时,*为指针定义符
②、在可执行语句指针之前,*为指针引用符
③、别忘了还有运算符的作用
&在不同地方的含义
①、在定义变量中,&为引用定义符,且必须初始化引用。
②、在可执行语句中,&为取址符
3.指针指向函数
int (*funP)(int a) = fun1;此处就是一个指向函数的指针(函数返回值类型+(指针变量名)+(函数传入的参数)=函数名)
作用:可以实现在函数中传递函数的操作,原理是函数的一个参数是函数指针,然后就可以直接传递函数了。
示例:
#include <iostream>
#include "Staff.hpp"
//此函数中添加函数的指针参数,并且在函数中进行函数调用
int funTest(int a, int b, int (*callBack)()) {
printf("参数%d\n", a);
int ret = (*callBack)();
printf("参数%d\n", b);
return 0;
}
//下面是三个函数的不同实现
int func1()
{
printf("调用了func1方法\n");
return 0;
}
int func3()
{
printf("调用了func3方法\n");
return 0;
}
int func2()
{
printf("调用了func2方法\n");
return 0;
}
//在函数中调用不同的三个函数指针
int main()
{
funTest(0, 1, func1);
funTest(0, 1, func2);
funTest(0, 1, func3);
return 0;
}
结果如下:
参数0
调用了func1方法
参数1
参数0
调用了func2方法
参数1
参数0
调用了func3方法
参数1
所以上面的函数类似于回调函数,钩子函数
4.局部变量,全局变量,静态变量
局部变量:代码块中定义的变量是局部变量,生命周期随着函数的执行完成而结束
全局变量:全局变量是在所有函数体的外部定义的,程序的所有部分都可以使用。全局变量不受作用域的影响,其生命周期一直到程序的结束。
静态变量:在代码块中定义的变量,用static修饰,作用域是函数体内(和局部变量一致),但是生命周期是一直到程序结束(和全局变量一直)
int funA()
{
static int a = 0;
int b = 0;
a++;
b++;
printf("a: %d\n", a);
printf("b: %d\n", b);
return 0;
}
int main()
{
funA();
funA();
funA();
funA();
funA();
return 0;
}
打印结果:
a: 1
b: 1
a: 2
b: 1
a: 3
b: 1
a: 4
b: 1
a: 5
b: 1
如上可以看出静态变量生命周期没有随着函数体的执行结束而结束。
5.函数返回指针需要注意什么
函数在什么场景返回指针:例如在返回数组的情况下需要返回指针
如下:
int* funB()
{
int a[] = {1,2,3,4};
return a;
}
但是上面的写法会崩溃,因为a是局部变量,会随着funB结束而销毁,所以我们需要在堆内存中分配空间,如下:
int* funB()
{
int* a = (int*)malloc(4 * sizeof(int));//申请堆内存
return a;
}
int main()
{
int* a = funB();
free(a);//注意堆内存释放
return 0;
}
八.面向对象编程
和java kotlin类似,用class关键字,c++中有析构函数,在对象被销毁的时候调用,可以用于一些堆内存的释放,析构函数写法:~类名(参数)
c++的面向对象也是用抽象,继承,多态的特性
多态实现:需要用到virtual关键字将父类中的需要子类实现的函数修饰,这样在调用时是可以直接调用到子类的方法,实现多态
virtual int work(); 虚函数 该staff 类里也可实现
virtual int work()=0;纯虚函数,只声明,不实现。
虚函数能实例化对象。
纯虚函数的类只能被继承,不能被实例化,类似于java中的抽象方法
注意在实例化子类的对象的时候会调用到子类的构造函数和父类的析构函数,所以会出现子类的析构函数没被调用,这样就有可能出现问题,可能由于析构函数没有被调用而出现内存泄漏。
上面问题的解决方式是将父类的析构函数写成虚函数。
九.动态内存管理
内存管理
1.资源获得初始化原则(RAII)
构造函数 创建资源
析构函数 销毁资源
2.引用和指针
引用和指针功能一样的,可以理解是更加好写,更加规范的指针。
引用必须要初始化,赋值nullptr。
int &ra = a;
引用只能指向一个地址,而指针可以变化自己的指向地址。
引用和指针用法的区别。
3.赋值构造函数与赋值运算符重载
在C++中=是可以在类中进行重载的,如果不进行重载可能发生错误,如下
int main()
{
Buffer a;
Buffer b;
b = a;
return 0;
}
上面的代码将a赋值给到b,所以b指向的是a中的内存空间,所以在释放内存的时候a被调用两次(就是a的析构函数被调用两次),b的析构函数没有被调用,产生错误。
所以要在Buffer类中实现赋值运算符重载并用memcpy方法进行拷贝。
4.向函数中传递对象
-
若是直接在函数的参数列表中传递对象,那么这个传递到函数内部的对象仅仅是副本,造成资源浪费。
-
所以,一般选择使用指针或者引用作为函数列表中的对象引用
-
使用指针作为函数列表的参数时,为了增加健壮性还需要对指针进行判空操作。
-
因此可以选择使用引用传递对象
-
而且,可以使用const修饰来增加对象的存在周期,且指向不可修改。
5.智能指针
可以自动释放内存的指针称为智能指针,不需要调用delete方法释放指针
①.unique_ptr:只能在初始化的时候指向一片内存,之后他就不能被别人重新赋值,也不能赋值给其他指针,这是一个孤独的智能指针。
②.shared_ptr:可以赋值给其他指针,原理是使用了引用计数法,当一个内存被引用时引用计数+1,当引用计数为0时则自动调用delete方法进行销毁。
③.weak_ptr:解决shared_ptr中的一个特定情况,即相互引用不能被销毁的情况