C++从入门到上天

首先,感谢大家花时间来阅读我的文章,希望都能有所收获。因为内容比较多,还没写完,后面我会慢慢补充的~

内存的分区及分配方式

内存分区

  • 代码区: 来存放函数体的二进制代码。

  • 全局(静态)区: 存放静态变量(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(1010);	// 正确
	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,具体区别,看表:

publicprotectedprivate
类外可以不可以不可以
类内可以可以可以

代码:

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++中:

  • 堆内存是需要手动释放的,析构函数就提供了方便,如果不释放的话,就会造成内存泄漏,进而导致内存溢出;
  • 从程序运行可知,在栈内存的对象的析构函数的调用,和析构函数的调用顺序是相反的,即是一个栈结构顺序:先进后出。

继承

重写

多态

虚函数

虚析构函数

抽象类

纯虚函数

纯虚析构函数

运算符重载

友元

模板

函数模板

  满足条件之一:

  • 模板必须要能根据传递的参数推导出数据类型,不能具有二义性。
  • 模板必须要能确定类型

类模板

总结

  如果你能读到这,那真的是非常感谢,我会努力写出更多通俗易懂且有价值的博客的,如果需要转载,劳烦附上原文链接丫~ 阿里嘎多~

https://blog.csdn.net/weixin_44108562/article/details/121027388https://blog.csdn.net/weixin_44108562/article/details/121027388

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值