文章目录
首先,感谢大家花时间来阅读我的文章,希望都能有所收获。因为内容比较多,还没写完,后面我会慢慢补充的~
内存的分区及分配方式
内存分区
-
代码区: 来存放函数体的二进制代码。
-
全局(静态)区: 存放静态变量(static)、全局变量(extern)。由编译器管理,编译阶段自动分配,直到整个程序执行结束,才释放。另外,初始化的全局变量、静态变量在同一块区域,未初始化的全局变量、静态变量在相邻的另一块区域。程序结束后由系统释放。
-
栈区: 存放局部变量、自动变量、数组等等。由编译器管理,执行函数时,自动分配;函数执行结束后,自动释放。
- 优点: 效率高、速度快(因为自动管理)
- 缺点:内存有限、分配后不可变(因为自动管理)
-
堆区: 需程序员主动调用new、mollac分配和调用delete、free释放。这类变量,由程序员管理,生命周期由程序员决定,即自被分配起,直到被释放或者程序执行结束之前,一直存在。
- 优点:动态分配、自由灵活
- 缺点:管理不当容易造成内存泄漏,甚至内存溢出,导致程序崩溃。
-
常量区: 存放着常量字符串。
-
寄存器: 存放寄存器变量(register)。该变量,不能被取地址,因为存放在CPU寄存器中。该变量的最大尺寸等于寄存器的大小。不过这个很少用到。了解一下就好。
int a; // 全局/静态区
int main()
{
static int b; // 全局/静态区
int c; // 栈区
char str[] = "abc"; // 栈区
int *p1 // 栈区,p1本身被存放在栈区
int *p2 = (int*)mollac(sizeof(int)); // p2本身在栈区;p2存放的地址所指向的内容在堆区,即*p2在堆区
free(p2) // 所以p2指向的堆区内存需要手动释放
char *pstr = "def" // pstr在栈区,“def”被存放在常量区,即 p2存放的地址所指向的内容在常量区
register int r; // 寄存器
}
分配方式
分配方式有两种(我所知的!保命!):
- 动态分配方式: 即由程序员主动(手动)分配的方式。
- 静态分配方式: 即由编译器自动分配的方式。
请不要和上面的静态区搞混哈,这里说的是分配方式(怎么分),上面说的是内存分区(分在哪)
引用
引用的本质:
- 引用,其实是一个指针常量
- 引用,其实不占内存,因为已经已经经过编译器优化了,所以可以当作变量的别名
int main(){
int num = 9;
int& r = num; // 引用
int* const p = # // 常量指针
// 输num出值
cout << r << endl;
cout << *p << endl;
// 输出num的地址
cout << &r << endl;
cout << &(*p) << endl;
cout << p << endl;
// p的地址
cout << &p << endl;
//cout << &&(*p) << endl; // 会报错,不运行这样写
//cout << &&r << endl; // 会报错,不允许这样写
return 0;
}
通过代码,相信大家对引用有了更深的了解,我在刚学习的时候,也好奇引用是否占内存,所以尝试通过两级取地址来证明是否占内存,但是这样的写法是不被允许的,所以结果也就不知道了。不过我相信会有博客写它占用内存(通过逆向得到了汇编代码,发现引用自身有地址),也有写不占用(我就是其中一个)。但是我相信是不占用的,如果没有经过优化,那它的出现意义几乎可以忽略;如果占用的话,各类书籍就会写出来了,也不会让我们在这争来争去。所以,就当作一个好东西来用就行啦。
函数
默认参数
也就是在定义函数的时候直接给形参赋值,有实参传进来的时候,就实参优先;当没有实参传递的时候,形参就会使用这个默认值,所以不会报错。
void fun(int a=0, int b=0)
{
cout << a << " " << b << endl;
}
int main()
{
fun(10, 10); // 输出:10 10
fun(10); // 输出:10 0
fun(); // 输出:0 0
return 0;
}
占位参数
C++的函数,在定义的时候是可以不写形参名的,也就是只写类型,譬如:void fun(int a, int){}
,而第2个参数就是展位参数,在传参的时候是没有实际的形参来接收的。同时,在声明和使用的时候,参数一个也不能少
//声明函数,第二个参数不能少
void fun(int, int);
int main()
{
fun(10); // 会报错,因为需要两个参数,但只给了一个
fun(10,10); // 正确
return 0;
}
// 函数定义,第二个是占位参数
void fun(int a, int)
{
}
总而言之,就是占着茅坑不拉屎,这玩意具体有啥用,我还得好好想想,但是不要在C语言里边这么干,这是不允许的!!!
函数重载
函数重载就是:函数,在函数名相同的情况下,只要满足以下条件之一,就可以被重载:
- 参数类型不同
- 参数个数不同
- 返回类型不同
void fun(int a, int b)
{
cout << a << " " << b << endl;
}
int main()
{
fun(10, 10); // 输出:10 10
fun(10); // 输出:10 0
fun(); // 输出:0 0
return 0;
}
酱紫做的好处就是:
- 可以提高函数复用率
- 减少程序员命名和记忆的负担
类
类对象内存
- 空类对象: 没有成员变量和虚函数的类所实例化的对象。占1个字节。为了区分空对象所占内存的位置。
- 非空类对象: 含有成员变量时,就会按照实际的算,就不需要多出1个字节去区分了。但是要注意字节对齐。
需要注意的是,静态成员变量是不在类对象上的
class Cla1
{
public:
void fun()
{
int a;
}
};
class Cla2
{
public:
void fun()
{
int a;
}
int n;
static int m;
};
int main()
{
Cla1 t1;
Cla2 t2;
cout << "空类对象的内存:" << sizeof(t1) << endl; // 输出 1
cout << "非类对象的内存:" << sizeof(t2) << endl; // 输出 4 (int类型变量占4字节, 且m不属于t2这个对象)
return 0;
}
大家可能会好奇,fun函数里不是有一个变量a么,为什么它不占内存呢?不是它不占内存,而是还没有调用fun函数,所以它还没有被定义,也就没它什么事了;当调用fun函数的时候,在fun函数的生命周期里,它是存在且占内存的
权限
关于成员变量和成员函数的访问权限,class默认是private,struct默认public。面向对象的编程语言,基本上都是这三种,即public、protected、private,具体区别,看表:
public | protected | private | |
---|---|---|---|
类外 | 可以 | 不可以 | 不可以 |
类内 | 可以 | 可以 | 可以 |
代码:
class Cla
{
public:
void fun()
{
// 类内访问,都可以。
cout << a << " " << b << " " << c << endl;
}
int a = 1;
protected:
int b = 2;
private:
int c = 3;
};
int main()
{
Cla1 t;
t.fun(); // 输出:1 2 3
cout << "类外访问 a:" << t.a << endl; // 输出 1,public成员可以类外访问
cout << "类外访问 b:" << t.b << endl; // 报错,权限不够
cout << "类外访问 c:" << t.c << endl; // 报错,权限不够
return 0;
}
从表中和代码中,大家都可以清晰的直到了权限的使用,但是可能会好奇(感觉protected和private也没啥区别丫),这个问题,在继承中会讲到的。
this指针
每个对象都会有一个this指针,所指向的就是对象本身。如果学过python的小伙伴就会知道,python里有一个self,其实和this是类似的,主要作用就是:
- 解决名称冲突,即重名问题
- 可以用来返回对象本身
class Cla
{
public:
Cla(int a, int b){
// a = a; // 同名,可能会报错
// b = b;
this->a = a;
this->b = b;
}
Cla& fun()
{
cout << "c中this的值:" << this << endl;
return *this;
}
int a;
int b;
};
int main()
{
Cla c(10, 10);
Cla& t = c.fun();
cout << "c的地址:" << &c << endl;
cout << "t的地址:" << &t << endl;
return 0;
}
/*
输出
c中this的值:0x61fe10
c的地址:0x61fe10
t的地址:0x61fe10
*/
从程序中,可以看到:
- 类内声明了变量和构造函数的形参一样,在赋值时,可能会报错,因为重名了,编译器分不清谁是谁。当然如果没有报错的话,那可能就是你的编译器比较厉害(我就没有报错)。当然为了好区分建议不要重名,非要重名,建议用this来区分一下。
- 从输出中,可以看到输出的3个地址是一样的。对象c调用fun函数返回了this指向对象的引用(即c本身),赋值给了引用t。所以this指向的对象的地址,就是c本身的地址。
构造函数
构造函数,主要作用就是在实例化对象的时候为对象的属性变量赋值,完成一些初始化工作,创建对象是编译器自动调用。
-
构造函数没有返回值
-
构造函数的名字和类名相同
-
构造函数可以有参数,可以发生重载
-
程序创建对象的时候会自动调用,而且只会调用一次:
- 栈内存:对象定义的时候,就会调用
- 堆内存,申请堆内存(new)的时候,就会调用
-
可重写,也可不写。如果不写,编译器会默认是空实现
语法: 类名(){}
,通俗理解,可以把它当成一个没有返回值且自动调用的函数。
class Cla
{
public:
Cla(){
cout << "我是无参构造函数" << endl;
}
Cla(string str){
cout << "我是有参构造函数:" << str << endl;
}
};
int main()
{
Cla a;
Cla b("栈");
Cla *c = new Cla;
Cla *d = new Cla();
Cla *e = new Cla("堆");
Cla f(); // 因为这个没有参数传入,所以编译器认为是函数,返回值是Cla
return 0;
}
/*
输出
我是无参构造函数
我是有参构造函数:栈
我是无参构造函数
我是无参构造函数
我是有参构造函数:堆
*/
值得注意的是,在使用栈内存实例化无参对象时,要注意不要加括号,否则编译器会认为是函数声明;堆内存则没有这个问题。
拷贝构造函数
浅拷贝
浅拷贝,可以理解为直接将被拷贝对象的属性值,直接赋值给接受拷贝的对象的属性。这是拷贝构造函数函数默认的拷贝方式,不需要自己实现。不过只适用于存放在栈区的属性变量,不适合在堆区的。对象之间直接赋值时,赋值运算符默认重载的也是浅拷贝方式。为了方便大家理解,我显式的写出它们的代码:
class T {
public:
T(int num):m_num(num){
cout << "我调用了构造函数" << endl;
}
// 默认的拷贝构造函数(不写也可以)
T(const T& t){
cout << "我调用了拷贝构造函数进行浅拷贝" << endl;
this->m_num = t.m_num;
}
// 默认的赋值符号重载(不写也可以)
T& operator=(const T& t) {
cout << "我调用了重载赋值符进行浅拷贝" << endl;
this->m_num = t.m_num;
return *this;
}
private:
int m_num;
};
int main()
{
T t1(1);
T t2(t1); // 拷贝构造函数
T t3 = t2; // 拷贝构造函数
T t4(4);
t4 = t3; // 重载赋值符拷贝
}
/*
输出:
我调用了构造函数
我调用了拷贝构造函数进行浅拷贝
我调用了拷贝构造函数进行浅拷贝
我调用了构造函数
我调用了重载赋值符进行浅拷贝
*/
因为都是在栈区开辟的内存,都由编译器管理,所以不用考虑回收问题。但,当我们在堆区开辟内存时,还使用浅拷贝,那么在使用析构函数释放内存时就会出大问题:重复释放内存:
class T {
public:
T(int num){
this->m_pnum = new int(num);
cout << "我调用了构造函数, m_pnum的地址: " << m_pnum <<endl;
}
// 默认的拷贝构造函数
T(const T& t){
this->m_pnum = t.m_pnum;
cout << "我调用了拷贝构造函数, m_pnum地址: " << m_pnum <<endl;
}
// ~T(){
// if (m_pnum != NULL) {
// delete m_pnum;
// m_pnum = NULL;
// }
// }
private:
int* m_pnum;
};
int main()
{
T t1(1);
T t2(t1);
}
/*
输出:
我调用了构造函数, m_pnum的地址: 0x6641d0
我调用了拷贝构造函数, m_pnum地址: 0x6641d0
*/
大家发现了么,对象t2使用默认拷贝构造函数拷贝对象t1后,二者输出的指针m_pnum的地址是一样的,意味着它们
指向的是同一块地址内存。因为堆内存,我们是需要在析构函数中手动释放的,析构函数是自动调用。根据析构函数的调用规则:当t2,调用析构函数释放m_pnum,m_pnum已经为空,此时如果t1再调用析构函数释放m_pnum,那么就出问题了。所以这就是浅拷贝带来的问题。
深拷贝
深拷贝,为了解决浅拷贝的问题,是先在堆区重新开辟一块内存,再进行内容的拷贝,而不是像浅拷贝一样进行地址拷贝。
class T {
public:
T(int num){
this->m_pnum = new int(num);
cout << "我调用了构造函数, m_pnum的地址: " << m_pnum <<endl;
}
// 拷贝构造函数: 深拷贝
T(const T& t){
this->m_pnum = new int; // 先在堆区开辟新空间
*(this->m_pnum) = *(t.m_pnum); // 再复制内容
cout << "我调用了拷贝构造函数, m_pnum地址: " << m_pnum <<endl;
}
~T(){
if (m_pnum != NULL) {
cout << "我调用了析构函数释放m_pnum: " << m_pnum << endl;
delete m_pnum;
m_pnum = NULL;
}
}
private:
int* m_pnum;
};
int main()
{
T t1(1);
T t2(t1);
}
/*
输出:
我调用了构造函数, m_pnum的地址: 0x6a41d0
我调用了拷贝构造函数, m_pnum地址: 0x6a4210
我调用了析构函数释放m_pnum: 0x6a4210
我调用了析构函数释放m_pnum: 0x6a41d0
*/
使用深拷贝,就不会有重复释放内存的问题啦。这里只举例拷贝构造函数,在使用时,还要记得将赋值运算符重载成深拷贝的形式,代码内容和拷贝构造函数一样,多了一个引用返回。
静态成员函数及变量
静态成员函数和静态成员变量, 不属于具体的某一个实例,是所有对象共享,可以直接用类名去访问。
- 静态成员函数,它不能访问非静态的成员变量,因为非静态成员变量属于某个实例的,但是属于这个类的实例这么多,静态成员函数并不知道它具体是哪一个的。例如:动物类,可以有阿猫阿狗,如果你只告诉我体重,我咋知道这个属性是谁的。但是可以访问静态成员变量,因为静态成员变量是唯一的。
- 静态成员变量,在编译的时候就已经在全局区分配内存了。
析构函数
析构函数,和构造函数一样编译器自动调用,如果我们不显式实现,编译器会自动空实现:
-
执行一些与对象有关的清理工作
-
在对象被销毁前,自动调用
- 栈内存:在作用域内,程序执行结束,就会调用
- 堆内存:delete的时候,就会调用
语法: ~类名(){}
class C {
public:
C(int t) {
num = new int(t);
cout << "我是析构函数,被调用要创建num,*num=" << *num << endl;
}
~C() {
if (num != nullptr) {
cout << "我是析构函数,被调用要销毁num,*num=" << *num << endl;
delete num;
}
}
int* num;
};
int main() {
C c1(1);
C c2(2);
C c3(3);
C* c4 = new C(4);
delete c4;
return 0;
}
/*
输出:
我是构造函数,被调用要创建num,*num=1
我是构造函数,被调用要创建num,*num=2
我是构造函数,被调用要创建num,*num=3
我是构造函数,被调用要创建num,*num=4
我是析构函数,被调用要销毁num,*num=4
我是析构函数,被调用要销毁num,*num=3
我是析构函数,被调用要销毁num,*num=2
我是析构函数,被调用要销毁num,*num=1
*/
在C++中:
- 堆内存是需要手动释放的,析构函数就提供了方便,如果不释放的话,就会造成内存泄漏,进而导致内存溢出;
- 从程序运行可知,在栈内存的对象的析构函数的调用,和析构函数的调用顺序是相反的,即是一个栈结构顺序:先进后出。
继承
重写
多态
虚函数
虚析构函数
抽象类
纯虚函数
纯虚析构函数
运算符重载
友元
模板
函数模板
满足条件之一:
- 模板必须要能根据传递的参数推导出数据类型,不能具有二义性。
- 模板必须要能确定类型
类模板
总结
如果你能读到这,那真的是非常感谢,我会努力写出更多通俗易懂且有价值的博客的,如果需要转载,劳烦附上原文链接丫~ 阿里嘎多~