cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数

1  函数重载

1.1  定义

        要求:

        1)同一作用域内

        2)函数名相同

        3)形参表不同(与形参个数及每个形参类型有关,与形参名无关)

        重载关系的函数调用哪个:

        根据实参类型和形参类型进行匹配,调用最匹配的函数

// overload_pre.cpp
// 函数之间的关系--重载关系(1.同一作用域内  2.函数名相同  3.形参表不同)
// 形参表是否相同 与 形参名无关 与 形参的个数 以及 每一个对应形参的类型有关
#include 
using namespace std;

void foo( char* c, short s ) {
    cout << "1. foo" << endl;
}
void foo( int i, double d ) {
    cout << "2. foo" << endl;
}
void foo( const char* c, short s ) {
    cout << "3. foo" << endl;
}
void foo( double d, int i ) {
    cout << "4. foo" << endl;
}

// int foo( double i, int d ) {} // error-是否为重载关系和返回值类型无关
// void foo( double i, int d ) {} // error-形参表是否相同 与 形参名无关
int main( void ) {
    char* c;    short s;
    foo( c, s ); // 1
    const char* cc;
    foo( cc, s ); // 3
    int i;  double d;
    foo( i, d ); // 2
    foo( d, i ); // 4
    return 0;
}

1.2  重载和隐藏:

        -只有同一作用域内的同名函数才涉及重载的关系

        -不同作用域的同名函数涉及的是隐藏关系(定义表隐藏可见表)

// overload.cpp 详谈同一作用域
#include 
using namespace std;
namespace ns1 {
    void foo( char* c, short s ) {
        cout << "1. foo" << endl;
    }
    void foo( int i, double d ) {
        cout << "2. foo" << endl;
    }
}
namespace ns2 {
    void foo( const char* c, short s ) {
        cout << "3. foo" << endl;
    }
    void foo( double d, int i ) {
        cout << "4. foo" << endl;
    }
}
int main( void ) {
    using ns2::foo; //名字空间声明,从这行代码开始ns2中foo引入当前作用域(出现在定义表中)
    using namespace ns1;//名字空间指令,从这行代码开始ns1中的foo在当前作用域可见(出现在可见表中)
    char* c;    short s;   
    foo( c, s ); // 第3个foo将第1个foo函数隐藏
    return 0;
}

1.3  重载匹配优先级

        1)普通方式调用重载关系的函数:

        完全匹配 > 常量转换 > 升级转换(小转大) > 标准转换(大转小) > 自定义转换 > 省略号匹配

        2)函数指针方式调用重载关系的函数:

        函数指针本身的类型决定其调用哪个版本的重载函数。

        工作中建议完全匹配。

// overload2.cpp 重载匹配优先级
#include 
using namespace std;

void foo( char* c, short s ) { // _Z3fooPcs 完全匹配
    cout << "1. foo(char*, short)" << endl;
}
void foo( const char* c, short s ) { // _Z3fooPKcs 常量转换
    cout << "2. foo(const char*, short)" << endl;
}
void foo( char* c, int s ) { // _Z3fooPci 升级转换(小转大,没有数据损失)
    cout << "3. foo(char*,int)" << endl;
}
void foo( char* c, char s ) { // _Z3fooPcc 标准转换(大转小,可能数据损失)
    cout << "4. foo(char*,char)" << endl;
}
void foo( ... ) { // _Z3fooz 省略号(可变长)匹配
    cout << "5. foo(...)" << endl;
}
int main( void ) {
    char * c;   short s;
    foo( c,s ); //_Z3fooPcs(c,s) 
    // 普通方式调用重载关系的函数,根据实参类型和形参类型匹配,来确定调用哪个foo

    void(*pfunc)(const char*,short) = foo; // _Z3fooPKcs  定义函数指针,形参可无名
    pfunc(c,s); // 函数指针方式调用重载关系的函数,
                // 根据函数指针本身的类型,来确定调用哪个foo
    return 0;
}

        注意上述代码,定义函数指针,可以不写形参名。 

1.4  重载揭秘

        重载是通过C++换名机制来实现的:

        nm a.out 命令 查看函数符号名

        cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数_第1张图片

        通过extern  "C" 可以要求C++编译器按照C方式编译函数,即不做换名,当然也就无法重载

// extern/cal.cpp
extern "C" {                      //extern "C" {} ,缩不缩进都可
    int add( int a, int b ) {
        return a + b;
    }

    int sub( int a, int b ) {
        return a - b;
    }
}
// extern/main.c
#include 

int main( void ) {
    int c = add( 5, 3 );
    int d = sub( 5, 3 );
    printf("c=%d,d=%d\n", c, d);
    return 0;
}
//g++ -c cal.cpp  
//nm cal.o  //由于extern "C",函数没换名,与c代码中一致
//gcc -c main.c
//nm main.o
//gcc main.o cal.o  
//./a.out

2  动态内存(堆内存)分配

        可以继续使用标准C库函数malloc()/free(),

        free(野指针)后果很严重(段错误,double free),free(空指针)安全:

// new.cpp 动态(堆)内存分配
#include 
#include 
using namespace std;

int main( void ) {
    int* pm = (int*)malloc( 4 );
    cout << "*pm=" << *pm << endl; // 初始值为0
    free( pm ); // 当这行代码执行结束后,pm指向的堆内存被释放,进而pm变为野指针
    pm = NULL;  // pm变为空指针
    free( pm ); // 给free传递的为野指针,释放野指针后果很严重,释放空指针是安全
    return 0;
}

        更建议使用new/delete操作符在堆中分配/释放内存:

                int*  pi  =  new  int;         //初始值一般为0

                delete  pi;

        在分配内存的同时初始化

                int*  pi  =  new  int( 100 );   //初始值为100

        可以数组方式new:

                int*  pi  =  new  int [4] {10, 20, 30, 40};  // {}方式是11标准才支持的,编译时-std=c++11

                想申请16字节,实际多申请4字节,存储数组元素的个数

        但也要数组方式delete:

                delete[]  pi;                            加[],才能将多申请的4字节也释放掉

        通过new操作符分配N维数组,返回N-1维数组指针

                int (*pa) [4] = new int [3][4];  // 返回值类型是 int (*)[4]

                int (*pb) [4][5] = new int [3][4][5];  // 返回值类型是 int (*)[4][5]

        不能通过delete操作符释放已释放过的内存。

        delete野指针后果很严重(段错误,double free),delete空指针安全。

        故建议释放指针指向的内存后,立即置空:   

                delete(pn);     

                pn = NULL;

        new操作符申请内存失败,将抛出异常 。

// new.cpp 动态(堆)内存分配
#include 
#include 
using namespace std;

int main( void ) {
    int* pm = (int*)malloc( 4 );
    cout << "*pm=" << *pm << endl; // 初始值为0
    free( pm ); // 当这行代码执行结束后,pm指向的堆内存被释放,进而pm变为野指针
    pm = NULL;
    free( pm ); // 给free传递的为野指针,释放野指针后果很严重,释放空指针是安全

    int* pn = new int(100);
    cout << "*pn=" << *pn << endl; // 可以自己指定初始值为100
    delete pn; // 当这行代码执行结束后, pn指向的堆内存被释放,进而pn变为野指针
    pn = NULL;
    delete pn; // 给delete传递野指针,释放野指针后果很严重,释放空指针是安全

    int* parr = new int[4]{10,20,30,40};//以数组方式new一块内存,永远返回第1个元素的地址
    for( int i=0; i<4; i++ ) {
        cout << parr[i] << ' ';
    }
    cout << endl;
    delete[] parr; // 数组方式new的也要以数组方式delete

    // 不管是几维数组,都应该当做一维数组看待
    int(*p)[4] = new int[3][4]; // 返回值是一维数组类型的指针
    delete[] p;

    try {
        new int[0xFFFFFFFF];
    }
    catch(...) {  //捕获...
    
    }
    return 0;
}
//g++ new.cpp -std=c++11

3  左值和右值

        C++所有数据,不是左值,就是右值:

        左值:能够取地址的值,通常具名

        右值:不能取地址的值,通常匿名

// lrvalue.cpp 左值 和 右值
#include 
using namespace std;

int foo( ) {
    int m=888;
    return m;
}

int main( void ) {
// 当前作用域的生命期
// 具名内存-->能够取址-->左值|非常左值(无const修饰)
//                           |常左值  (有const修饰)
    int a = 10;
    &a;
    a = 15;

    const int b = 10;
    &b;
//  b = 15; // error

// 语句级生命期
// 匿名内存-->不能取址-->右值|直接更改右值毫无意义(98/03标准给出结论)
//
    10;
//  &10; // error
//  10 = 15; // error

    /*|888|*/foo( ); // (1)分配一块内存空间  (2)生成跳转指令
//  &foo( ); // error
//  foo( ) = 15; // error
    return 0;
}

4  引用(如影随形,从一而终)

        1)引用即内存的别名

                int  a = 10;  // a是内存的真名

                int&  b = a;  // 不是赋值,而是给a起别名!(给引用b,赋真名)

        2)C++层面,引用本身不占内存并非实体

              对引用(别名)的所有操作都是在对目标内存进行操作。

        3)引用必须初始化,且不能更换目标

                int  c = 20;

                b = c;  // 仅仅是对引用的目标内存a进行赋值

        4)不存在引用的引用

                int  a = 10;

                int&  b = a;   //别名b,真名a

                int&  d = b;   //别名d,真名a

        5)引用的常属性必须和目标的常属性“一致” 

                const  int  e = 10;

                const  int&  f = e;   // OK

                int&  g = e;   // ERROR

        6)可以限定更加严格

                int  a = 10;

                const int& h = a; // OK

// alias.cpp 引用:就是一块内存的别名
#include 
using namespace std;

int main( void ) {
    int a = 10;
    int& b = a; // 这并不是利用a的数据给b赋值,而应该理解为 引用b是a所代表内存的别名

    b = 20; // 对 引用b赋值,其实就是在对引用b的目标内存(a)赋值
    cout << "a=" << a << ", b=" << b << endl;//读取引用b的值,
                                             //其实读取的为引用b的目标内存(a)的值
    cout << "&a:" << &a << ", &b:" << &b << endl;
    // 取引用b的地址,其实取的为引用b的目标内存(a)的地址
    
    int c = 30;
    b = c;
    cout << "a=" << a << ", b=" << b << ", c=" << c << endl;
    cout << "&a:" << &a << ", &b:" << &b << ", &c:" << &c << endl;

    int& d = b; // 这并不是引用的引用,而应该理解为 d和b都是目标内存(a)的别名
    cout << "&a:" << &a << ", &b:" << &b << ", &d:" << &d << endl;
    
    const int e = 10;
//  int& f = e; // error,别名不可以比真名限定的更加宽松
    const int& g = e; // ok
   
    const int& h = a; // ok,别名可以比真名限定的更加严格
    return 0;
}

        7)引用可以延长右值的生命周期 ,(不非得是常引用,11标准...)

        8)常引用  即  万能引用 (常指针  亦即  万能指针)

// alias2.cpp  左值 / 右值 和 引用
#include 
using namespace std;

int foo( ) {
    int m=888;
    return m;
}

int main( void ) {
// 当前作用域的生命期
// 具名内存-->能够取址-->左值|非常左值(无const修饰)
//                           |常左值  (有const修饰)
    int a = 10;
    int& ra = a; // ok
    const int& cra = a; // ok

    const int b = 10;
//  int& rb = b; // error
    const int& crb = b; // ok

// 语句级生命期 (引用可以延长右值的生命期)
// 匿名内存-->不能取址-->右值|直接更改右值毫无意义(98/03标准给出结论)
//                           |到了11标准会有所不同??????
    const int& ri = 10; // ok

    const int& rf = /*|888|*/ foo( ); // ok
    
    return 0;
}

5  内联函数

        调用普通函数(非内联函数)的问题:

        -每个普通函数调用语句都需要发生跳转操作,这种跳转操作会带来时间开销

                cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数_第2张图片

        内联就是用函数已被编译好的二进制代码,替换对该函数的调用指令。

        内联在保证函数特性的同时,避免了函数调用的时间开销

                cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数_第3张图片

// inline.cpp 内联函数:编译器的优化策略 
#include 
using namespace std;

void foo( int x ) { // 非内联(普通)函数
    cout << "foo(int): " << x << endl;
}

inline void bar( int x ) { // 内联函数
    cout << "bar(int): " << x << endl;
}

int main( void ) {
    
    foo( 10 ); // 将此处替换为 跳转指令
    foo( 20 ); // ...
    foo( 30 ); // ...
    bar( 10 ); // 见此处替换为 bar函数编译后产生的二进制指令集
    bar( 20 ); // ... 
    bar( 30 ); // ...
    return 0;
}

        内联会使文件的体积变大,进而导致进程的内存变大,因此只有频繁调用简单函数才适合内联。

        稀少被调用的复杂函数递归函数都不适合内联。

        inline关键字仅表示期望该函数被优化为内联,但是否适合内联则完全由编译器决定。

cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数_第4张图片

cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数_第5张图片

cpp_02_函数重载_动态内存分配_左值右值_引用_内联函数_第6张图片

你可能感兴趣的:(Cpp,c++)