[C++从入门到精通] 2.inline内联函数、const的相关用法

简介: [C++从入门到精通] 2.inline内联函数、const的相关用法

一、返回类型

前置类型: 在函数声明和定义的时候,把函数返回类型写到函数名字之前的形式,叫前置返回类型

void func(int a); //函数声明
void func(int a)  //函数定义
{
  return;
}

后置类型: C++11中,在函数声明和定义的时候,把返回类型写在参数列表之后的形式,叫后置返回类型

auto  func1(int a) -> void; //函数声明
auto  func1(int a) -> void  //函数定义
{
  return;
}

作用差异: 随着对类这种概念学习的不断深入,有一些成员函数返回类型非常复杂,这时使用后置返回类型会使得代码看起来比较清爽,容易看懂。

PS:建议函数声明放在.h文件,函数定义放在.cpp文件中,如果函数定义放在头文件A.h中,当B.cppC.cpp都包含A.h的时候,会报错:找到一个或多个多重定义符号(函数重复定义)。


二、内联函数inline

定义: 在函数定义前,增加关键字inline,会使该函数变成内联函数。

适用: 当函数体很小,并且被频繁调用的函数,适合成为inline函数被调用。

解析: 大家都知道,调用函数是需要消耗系统资源的,尤其是函数体很小,只有一两行的代码,但是我需要一个循环频繁调用,这种频繁调用一个函数体很小的小函数是很不划算的,因为调用一个函数需要将函数形参压栈(开辟内存),往堆栈里面放数据,函数调用完之后返回出栈,这种调用过程需要频繁消耗内存,可能函数代码执行只用了0.1s,但是压栈出栈就用了1s,所以压栈出栈这种内存操作很不划算。

用法:

//.h
inline int func2(int test)
{
  return 1;
}

特性:

1、在编译阶段,当编译器识别到函数为inline函数时,系统会尝试将调用inline函数的动作替换为函数本体,以此来提升性能。

int main()
{
   int abc = func2(5); //当编译器识别到func2为内联函数inline时
   int abc = 1;        //系统会尝试用int abc = 1(函数本地);来取代int abc = myfunc(5)(调用内联函数的动作);前者毫无疑问比压栈出栈这种正常的函数调用快很多
}

2、inline函数只是我们开发者对编译器的一个建议,编译器可以尝试去做,也可以不去做,这取决于编译器的诊断功能,就是说,决定权在编译器,我们控制不了。

3、上面我们说过函数声明要放到.h文件中,函数定义放在.cpp文件中。而内联函数的恰恰相反,内联函数的定义就要放到.h文件中,这样需要用到内联函数的.cpp文件时,能够通过#include直接把内联函数的源代码#include进来。以便编译器找到函数本体的源代码,并尝试使用函数体内的实现语句替换函数的调用,达到更高效的目的。

缺点以及规避:

1、代码膨胀问题:内联函数的实现语句较多

如:

//.h 内联函数func2定义 
inline int func2(int test)
{
    int num;
    num= test;
  return num;
}

而我们调用内联函数的语句只有一句:

int main()
{   
    int temp = func2(5);
    return 0;
}

即上面这种情况我们使用内联函数时,需要用内联函数的函数体(3句)替换一句调用函数(1句),很明显不太划算,造成了代码膨胀。

在举个夸张一些的例子,假设某函数的函数体很多,执行需要1000s,那么将其转换成内联函数其实也就节省个压栈出栈的时间,比如1s,那么整体来说使用内联函数与否已经意义不大了,所以我们一般要求内联函数的函数体要尽量小一些。

注意:各种编译器对inline函数的处理各不相同,有的可能比较智能,有的编译器看你内联函数有计算什么的比较复杂,可能会直接当成普通函数处理。所以要求inline函数尽量简单,代码尽可能少,尤其是涉及到循环、分支、递归调用等语句、尽量不要出现在inline函数中,否则编译器很可能会因为你写这些代码而拒绝让这个函数成为一个inline函数。

拓展:

constexper函数,也要求函数体比较简单,否则会报错,我们可以看成是更严格的一种内联函数。

#define宏展开也类似于内联函数,但也有一些,比如类型检查方面的差别,这里不再展开说明,感兴趣的自行查阅。


三、函数杂合用法总结

1、函数返回类型为void,表示无返回值类型。但我们可以调用一个返回类型是void的函数,让它作为另一个另一个返回类型是void函数的返回值。

void func_a()
{
    return;
}
void func_b()
{
    //return;       //可以
    return func_a();//这也可以
}

2、函数返回指针和返回引用的情况

情景1: 函数返回指针类型

int *func_c()
{
    int tempValue = 9;
    return &tempValue;  //这不可以,因为函数执行完毕后,tempValue的内存被系统回收,不能在继续使用,返回出去毫无意义。
}
int main()
{
    int *p = func_c();
    *p = 6;            //你往一个不属于你的地址写了数字。    
    return 0;
}

大家想一想上面的指针函数调用会出现什么问题?

上面的代码虽然可以正常编译通过,这时调用指针函数func_c()时,系统会为函数内部的临时变量tempValue分配一段内存地址,但是当调用结束时,临时变量tempValue的地址会被系统回收,这时int *p = func_c();中指针p指向的是一段你不能操控的地址。*p = 6;虽然将6写进了指针p指向的地址中,但是这段地址是不属于你的,造成的后果就是轻则运行一段时间啪—崩溃,重则运行时立马啪—崩溃。。

解决:将局部变量tempValue改成全局变量,这样其不会因为函数执行结束而被系统回收内存,至始至终都会保留其内存地址。

情景2: 函数返回引用类型

int &func_d()
{
    int tempValue = 9;
    std::cout << "tempValue内存地址:" << &tempValue << std::endl;
    return tempValue;  
}
int main()
{
    int &k = func_d();
    std::cout << "k内存地址:" << &k << std::endl;
    k = 7;         //你往一个不属于你的地址写了数字。        
    return 0;
}

和情景1类似,都是往一个不属于你的地址写了数字

可以看到,虽然k的地址和函数func_d返回的引用地址相同,但是这块地址在传给k之前已经被系统回收了,是不属于你的,所以不可以用。

还有其他调用方法:

int main()
{
    int k = func_d();     //返回值承接一个整型值
    std::cout << "k内存地址:" << &k << std::endl;
    k = 7;                //安全
    return 0;
}

相信这里大家已经懵逼了,明明函数func_d返回的是一个引用,为什么却能用一个整形值k来接?

不过确实有这种调用,大家知道就好了,我感觉应该是编译器内部做了相关的转换,将函数返回的引用内部转换成了整形值,我们可以看到k的地址已经不是函数func_d调用结束后被释放掉的的局部变量tempValue的地址了,而是被系统新分配了一块内存地址,所以可以对其的地址处进行存值。

3、没有形参可以保持形参列表为空,或者形参为viod类型

int func_e(void)
{
    return 1;
}

4、如果一个函数我们不调用的话,则该函数可以只有声明部分,没有定义部分。

5、普通函数定义只能定义一次(定义放在.cpp文件中),声明可以声明多次(每次include一个头文件,都相当于声明了一次该头文件的函数)

6、C++中,更习惯用引用类型的形参来取代指针类型的形参,提倡在C++中,多使用引用类型的形参。

7、C++中,函数允许同名,但形参列表的参数类型或者数量应该有明显的区别。

void func_f(int a)      
{
    return;
}
void func_f(double b)      //可以,名称相同,形参类型不同
{
    return;
}
void func_f(int a, int b)  //可以,名称相同,形参数量不同
{
    return;
}

四、const char*、char const*、char* const三者的区别

💨 char * 的简单用法

char *p

int main()
{
    char str[] = "I Love China!";
    char *p;
    p = str;    //p指向str的地址 —— p的值为“I Love China!”
    *p = 'Y';   //p的首地址变为Y —— p和str都由“I Love China!”都变成“Y Love China!”
    p++;        //p向前移动一个地址 —— p的值变为“ Love China!”
    return 0
}

💫 const char * 的用法

const char *p:指针p指向的东西不能通过p来修改,即指针p所指向的内存地址,那块内存地址中的内容不能通过指针p来修改。

char *p代码改编:

int main()
{
    char str[] = "I Love China!";
    const char *p;
    p = str;      //p指向str的地址 —— p的值为“I Love China!”
    *p = 'Y';     //错误,因为p所指向的对象地址内容不能通过p来修改。
    p++;          //可以,地址被改变,并且p向前移动一个地址 —— p的值变为“ Love China!”
    str[0] = 'Y'; //可以,虽然p所指向的地址内容不能通过p来修改,但可以通过str来修改,p和str仍然都由“I Love China!”都变成“Y Love China!”。   
    return 0;
}

PS:

p++;          //原文我写的是地址没有被改变,谢谢AmbitionToFree的指正

💦 char const*的用法

char const*等效于const char * :指针指向的地址内容不能通过指针本身来修改。

 

💢 char * const的用法

char * const p:指针p一旦指向了一个东西之后,就不可以再指向其他的东西了(注意区分上面两个用法),即p指向一个地址后,从始至终都要指向这个地址。

char *p代码改编:

int main()
{
    char str[] = "I Love China!";
    char * const p        //错误,char * const定义的时候必须初始化
    char * const p = str; //正确
    *p = 'Y';             //正确,与const char *和char const*不同,p可以修改p指向的目标中的内容
    p++;                  //错误,p已经指向了str的地址,不可以在指向其他的内存地址。
    return 0
}

总结:

  • const char *char const*:指针指向的地址内容不能通过指针本身来修改。————属性:威武不能屈(const前置、中置)
  • char * const:指针的指向不能变,从始至终要指向一块内存地址。————————— 属性:富贵不能移(const后置)
  • const char * const p = strchar const * const p = str也要了解:表示p指向的内容不能通过p来改变,p的指向也不能变,非常的憋屈。

🌊 其他引用类型的const用法:同指针类型的const用法异曲同工

特性一:常量引用值不可以被修改

int main()
{
    int i = 100;
    i = 100;            //可以,对i没限制 
    const int &a = i;   //const前置,自动触发威武不能屈属性,a的内容不能被修改。
    //a = 500;          //错误,a的内容不能被修改  
}

特性二:允许为一个常量引用绑定非常量的对象、字面值、甚至是一个一般表达式

int main()
{
    int i=23;
    const int &r1=i;        //允许将const int& 绑定到一个普通的int对象
    const int &r2=23;       //允许将const int&绑定到字面值常量
    const int &r3=r1*2;     //允许将const int&绑定到表达式
    int &r4=r1*2;           //错误,r4是一个非常量引用
}

五、函数形参中带const

上一篇我们学习引用做函数形参时讲过,对函数中形参的内存地址(内容)进行更改就相当于对函数外部实参的内存地址(内容)进行修改了。

那么我们不想因为无意修改函数内部形参的值而导致外界实参值发生了改变,应该什么办呢?

可通过在函数形参前加const解决上面的问题:

struct Student {int num};
void num(Student &stu)
{
    stu.num = 1010;    //可以,修改后函数外部Student的对象值也被改成1010。
}
void num_2(const Student &stu)
{
    //stu.num = 1010;  //不可以,因为const修饰对象stu,形参stu的值不能被修改。
}

函数形参前加const的用法使用的很多,使用这种方式之后,我们就不会在函数内部无意修改形参的值了,我们应该学习这种形参中增加const的写法的习惯。

函数形参前加const还有一种好处就是可以使函数实参的类型更灵活,比如:

①函数形参前不加const修饰

int main()
{
    Student stu1;
    num(stu1);     //可以,正常调用
    const Student &stu2;
    //num(stu2);     //不可以,实参stu2的值不可以被修改,不可以将实参const Student &类型转换为形参Student &类型
}

②函数形参前加const修饰

int main()
{
    Student stu1;
    num_2(stu1);     //可以,正常调用
    const Student &stu2;
    num_2(stu2);     //可以,实参类型与形参类型一致
}

函数形参前加const修饰时,既可以接收实参是普通结构的数据类型(Student stu1),又可以接收实参是常量常量引用类型(const Student &stu2)。

总结:函数形参中带const的优势

  1. 可以防止你无意中修改形参值导致实参值被无意修改。
  2. 实参类型可以更灵活。

tip:实际应用时,函数形参能加const尽量加const,不过要是需要在函数内部对外界的实参做出修改,那还是需要正常的引用方式,看情况使用该技能。


戳戳小手帮忙点个免费的赞和关注吧,嘿嘿。


目录
相关文章
|
1天前
|
编译器 C语言 C++
C++入门基础-2
C++入门基础
11 3
|
1天前
|
C语言 C++
C++入门基础-1
C++入门基础
15 1
|
2天前
|
自然语言处理 编译器 C语言
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
本文章是我对C++学习的开始,很荣幸与大家一同进步。 首先我先介绍一下C++,C++是上个世纪为了解决软件危机所创立 的一项面向对象的编程语言(OOP思想)。
30 1
【C++】C++ 入门 — 命名空间,输入输出,函数新特性
|
3天前
|
存储 安全 编译器
【C++从练气到飞升】03---C++入门(三)
【C++从练气到飞升】03---C++入门(三)
|
3天前
|
存储 自然语言处理 编译器
【C++从练气到飞升】02---C++入门(二)
【C++从练气到飞升】02---C++入门(二)
|
3天前
|
Unix 编译器 C语言
【C++从练气到飞升】01---C++入门(一)
【C++从练气到飞升】01---C++入门(一)
|
3天前
|
安全 编译器 程序员
【C++入门到精通】C++类型的转换 | static_cast | reinterpret_cast | const_cast | dynamic_cast [ C++入门 ]
【C++入门到精通】C++类型的转换 | static_cast | reinterpret_cast | const_cast | dynamic_cast [ C++入门 ]
12 0
|
3天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
14 0
|
5天前
|
C语言 C++
【C++】string类(常用接口)
【C++】string类(常用接口)
13 1
|
2天前
|
编译器 C++
【C++】继续学习 string类 吧
首先不得不说的是由于历史原因,string的接口多达130多个,简直冗杂… 所以学习过程中,我们只需要选取常用的,好用的来进行使用即可(有种垃圾堆里翻美食的感觉)
7 1