从 C 到 C++

01 引用

引用的概念:某个变量的引用,等价于这个变量,相当于该变量的一个别名

引用的定义: 类型名 & 引用名 = 某变量名;

int param = 4;
int & alias = param; // alias 引用了 param, alias 的类型是 int &

引用与指针的区别

​ C++ 中对对象的定义为:对象是指一块能存储数据并具有某种类型的内存空间。一个对象 a,它有地址 &a;运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,并可以通过该对象的地址来访问其存储空间中的值。对象又有常量和变量之分,常量是指存储了固定值并具有某种类型的存储空间,可读不可写;而变量则是相对的,是指具有某种类型并可供操作的存储空间,可读也可写。

指针 p 也是对象,它同样有地址 &p 和存储的值 p,只不过,p 存储的数据类型是数据的地址。如果要通过 p 中存储的数据作为地址来访问该指针对象的值,则要在 p 前加解引用操作符 “*”,即 *p。指针作为一种对象也有常量和变量之分,指针常量 DataType * const p = param; 是指该指针对象存储的地址 &p 是不可改变的,但是可以修改该指针对象存储的值 p \*p = (DataType) value常量指针 DataType const * p = param; OR const DataType * p = param; 是指该指针对象存储的值 p 是不可改变的,即不能通过指针对象 *p 直接修改其存储的值;常指针常量 const DataType * const p = param; 是指针常量和常量指针的结合。

​ 根据引用的概念可以看出,C++ 中引用就是指针常量,定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。为此,引用有如下三个特点:

  • 在声明引用时一定要对其初始化
  • 引用经初始化后,就不可以再和其他对象绑定在一起了,一直引用该对象(从一而终)
  • 引用只能引用变量,不能引用常量和表达式,& 是取地址符

常引用

​ 常引用可以看作是常指针常量,常引用 const DataTye & 和非常引用(普通引用) DataType & 是不同的数据类型,普通引用和普通变量都可以用来初始化常引用,但是常变量 const DataType 和常引用不能用来初始化普通引用,除非进行强制类型转换

引用的应用

  1. 引用作为函数形参传值
// swap函数实现数值交换

// C 语言实现
void swap(int *a, int *b){
    int temp;
    temp = *a; *a = *b; *b = temp;
}

int main(){
    int m, n;
    m = 1; n = 2;
    swap(&m, &n);
    return 0;
}

// C++ 实现
void swap(int &a, int &b){
    int temp;
    temp = a; a = b; b = temp;
    // temp = a; 赋值的为什么不是 &a 地址值
}

int main(){
    int m, n;
    m = 1; n = 2;
    swap(m, n);
    return 0;
}
  1. 引用作为函数的返回值
int n = 4;
int & set_n(){
    return n;
}

int main(){
    set_n() = 100;
    cout << n;
    return 0;
}

02 动态内存分配

C语言中的动态内存分配

malloc 分配内存

​ C/C++中可以使用 malloc 开辟一块连续的内存空间,并返回一个指向被分配内存空间起始地址的指针 DataType * p = (DataType *) malloc(sizeof(DataType)*number);。malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的空闲链表,**即所谓的一级内存池。**内存池的作用是提高 malloc 向操作系统申请内存空间的效率。通过系统调用向操作系统申请/返还空间需要时间开销,所以 malloc 索性一次性申请 128K 的内存,当上层内存申请小于一级内存池的内存可用量时,直接返还,就不用和操作系统通信,从而提高效率。而实践中,小额的内存频繁申请的机会占大多数,所以内存池能够有效提高效率。

​ 调用 malloc 函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块;然后,将该内存块一分为二,一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节;接下来,将分配给用户的那块内存传给用户,并将剩下的那块返回到连接表上。如果用户申请一个较大的内存空间,malloc 的内存池中没有直接满足该大小的内存空间时,malloc 检查空闲链表上的个内存片段,并对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果最后仍然无法获得符合要求的内存块,malloc 函数会返回 NULL 指针。

​ 操作系统为进程分配的内存中往往包含一块用户空间 (User Space),用户空间中分为多个内存段主要包含以下几段:

  • 程序段 Code Segmentation:位于整个用户空间的最低地址部分,存放指令即程序所编译成的可执行机器码
  • 数据段 Data Segmentation:存放初始化过的全局变量
  • BSS 段 BSS Segmentation:存放未初始化的全局变量
  • 堆 Heap:用于存放进程运行中被动态分配的内存段,堆的大小不是固定的,可以动态变化。当进程调用 malloc / free 等函数分配内存时,新分配的内存就被动态添加到堆上,或将被释放的内存从堆中剔除。
  • 栈 Stack:存放程序的局部变量,在函数调用过程中也是通过栈来传递参数和返回值,利用先进后出特性方便保存和恢复调用现场。

​ malloc 函数的申请内存空间就是通过系统调用从操作系统(即堆 Heap 空间中)获得,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表,当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

free 释放内存

​ C/C++中可以使用 free 释放内存空间,调用free函数时,它将用户释放的内存块连接到空闲链上,即将不用的内存返还给 malloc 维护的一级内存池。值得注意的是,free 释放的是指针指向的内存,而非指针本身,指针作为一个对象,只有程序运行结束时才被销毁。释放了内存空间后,原来指向这块空间的指针还是存在,只不过现在指针指向的是未定义的内容。因此,在释放内存后,要把指针指向 NULL,防止指针被解引用。

C++ 中的动态内存分配

new/delete 动态管理对象

​ 相较于 malloc/free,new/delete 可以更加自动化地申请和释放内存,因为 new/delete 在对象创建的时候自动执行构造函数,对象消亡之前会自动执行析构函数。如下示例代码所示,在编译器层面针对各种对象,系统调用了默认的构造函数来初始化以及默认的析构函数来析构,这种方式能保证对象一被创建出来便被初始化,出了作用域便被自动清理。

// 调用示例
Complex *pc = new Complex(1,2);
...
delete pc;

// 编译器优化:在编译器中对上述代码过程进行优化如下
// new 过程
Complex *pc;
try{
    void *mem = operator new(sizeof(Complex)); // 申请内存空间
    pc = static_cast<Complex*>(mem);
    pc->Complex::Complex(1,2); //调用构造函数
} catch(std::bad_alloc) {
    // 失败就不执行构造函数
}
// delete 过程
pc->Complex(); // 调用析构函数
operator delete(pc); // 释放内存空间

​ 从本质去看待 new/delete 和 malloc/free,前者就是对后者的一层封装。上述示例中的 operator new();operator delete(); 的源码实现如下所示:

void *operator new(size_t size, const std::nothrow_t&)_THROW0()
{
    void *p;
    while((p = malloc(size)) == 0){
        _TRY_BEGIN
            if(_callnewh(size) == 0) break;
        _CATCH(std::bad_alloc) return (0);
        _CATCH_END
    }
    
    return(p);
}

void _cdecl operator delete(void *p)_THROW0()
{
    free(p);
}

​ 在 new 中,先由 operator new();调用 malloc 根据对象申请数据存储大小(非全局静态成员变量+虚函数表指针大小*数量)的空间,然后赋给一个空指针,并将静态转型为目标型的指针,赋值给刚开始定义对象类型指针,最后调用了类的构造函数,对内对中的对象空间初始化赋值。在 delete 中,则是先调用了对象的析构函数,然后通过 operator delete(); 调用 free 来释放空间。

new[]/delete[] 动态管理对象数组

​ 动态分配一个数组的声明方式 string *psa = new string[10]; int *pia = new int[10]; 在 string 数组中分配了保存对象的内存空间之后,将调用 string 类型的默认构造函数依次初始化数组中每个元素;而 int 是内置类型的数组,该过程中分配了存储 10 个 int 对象的内存空间,但并没有初始化。

​ 释放对象数组空间的声明方式 delete [] psa; delete [] pia; 在 string 数组中对 10 个 string 对象分别调用析构函数,然后调用operator delete[]();,该函数中调用operator delete(); ,由上述代码可知delete函数底层用 free 释放掉为对象分配的所有内存空间;而 int[] 是内置类型不存在析构函数,直接释放为 10 个 int 型分配的所有内存空间。

​ new/delete new[]/delete[]的调用过程如下图所示:(图片来源网络,如有侵权,请及时告知)
在这里插入图片描述

03 C++的函数

内联函数 inlineinline int Max(int a, int b);

​ 函数调用过程中存在一定的系统开销,为了减少函数调用的开销,C++ 中引入了内联函数机制。如果函数本身语句较少,执行时长很短,但是函数被反复调用,那么可以采用内联函数。编译器在处理对内联函数的调用语句时,直接将整个被调函数的代码块插入到调用语句处,而不会产生调用函数的语句。

​ 内联函数的优点在于:a) 在调用时直接替换,减少了函数调用的开销,提高了代码执行效率。b) 编译器在处理内联函数时会对其参数类型进行检查,这样可以消除替换过程中的安全隐患和局限性。c) 内联函数可以作为一个类的成员函数,与类的普通成员函数作用相同,可以访问一个类的私有成员和保护成员。

​ 内联函数的局限性在于函数体不能太大,inline 对编译器只是一种建议,当内联函数的函数体过大时,编译器会选择忽略这种建议,采用普通的函数调用方式执行程序,这就失去了声明内联函数的意义。

函数重载

​ 一个或多个函数,名字相同,参数个数或参数类型不同,这种方式叫做函数的重载。函数名字可以相同使得编码过程中函数命名变得简单;在编译过程中,编译器根据调用语句中实参的个数和类型判断应该调用哪个函数。但是函数重载在调用过程中要避免二义性,这会导致编译错误,示例如下所示:

int Max(double d1, double d2){} // (1)
int Max(int n1, double n2){} // (2)
int Max(int n1, int n2, int n3){} // (3)
// 调用
Max(3.4,2.5); // (1)
Max(2,3); // (2)
Max(1,2,3); // (3)
Max(2,3.4); // error 存在二义性

函数的缺省参数

​ 在 C++ 中定义函数的时候可以让最右边的连续若干个参数有缺省值,在调用函数的时候,若相应位置不写参数,参数就是缺省值。函数参数可缺省的目的在于提高程序的可扩充性,为未来可能要扩充的函数参数预留位置;如果某个已经写好的函数需要添加新的参数,而原先调用该函数的语句可能不需要使用新增的参数,那么为了避免溯源对调用语句进行修改,使用缺省参数达到目的。

void func(int x1, int x2 = 2, int x3 = 3){}
// 调用
func(10); // complete!
func(10,8); // complete!
func(10, ,8); // error, 不能间隔参数缺省

Reference

C++引用与指针的区别

C++动态内存分配,malloc 与 new 的区别

深入理解C++ new/delete, new []/delete[]动态内存管理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王清欢Randy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值