本博客将记录:函数新特性、内联函数、const关键字详解这3个方面的知识点笔记!
今天总结的知识分为5点:
一、函数回顾与后置返回类型
二、内联inline函数
三、函数杂合用法总结
四、const char* 、char const* 、char * const 三者的区别
五、函数形参中带const
一、函数回顾与后置返回类型
一般的函数定义格式:(按照前置返回值类型来定义)
returnType Function_Name(params){}
比如:
void func(int, int);//函数声明(函数原型)中的形参可以不带变量名字
void func(int a, int t) {//但是函数定义时的形参表中必须要带上变量名字
return;
}
后置返回值类型:是C++11新引入的一种语法特性,因为我们在类中写成员函数时往往函数的返回值类型会过长,因此我们可以用以下的格式来进行函数定义:
auto Function_Name(params) ->returnType(这就是后置函数返回值类型的语法格式)
(auto关键字:自动推断变量类型.用在这里我们可理解为推断函数的返回值类型为->符号之后的类型)
比如:
auto funcc(int a, int b)->void;//函数的返回值类型为void
auto funcc(int a, int b)->void {//函数的返回值类型为void
return;
}
注意:不论是前置还是后置函数的返回值类型,本质上这2种写法都是等价的!
二、内联函数(之前我coding时忽略的点!必须要重视这个小知识点之学习,但是内联函数这一个知识点是一块庞大的知识面,且对于不同的编译器,其具体的内联实现是不同的,因此啊,目前这个coding阶段我只要把下面这些知识点掌握好了解clear了就足够了!)
相信普通函数我们都写过,比如:
bool isEmpty(){return true;}//normal funciton
那么,什么又是内敛函数呢?
答:普通函数的定义前 加上inline关键字之后,那么这样普通函数就会变成内联函数了。
比如:
inline isEmpty(){return true;}//内联 funciton
那么为什么C++11要引入内联函数呢?因为我们平时coding时往往会写一些函数实现本来就没几行代码的函数,此时,在编译运行时既要对这些个"小"函数进行各种变量的保存,无论是临时变量保存在系统栈中还是别的case,当用完这个函数之后,回顾整个过程,是需要do一些压栈以及出栈操作的,那在我们看来,和"大"函数相比,这些"小"函数频繁进行压栈出栈等操作很没有必要。因为这样do很低效!
因此,对于函数实现体很小,被调用很频繁的这些个函数,咱们引入了inline(内联函数)关键字。
inline关键字(内联函数)的作用和注意事项: (本质上就是为了减少函数的压栈进栈,提升效率)
1)inline会使得编译器将调用该内联函数的动作替换为函数的本体(或者说是函数的返回值语句)
inline关键字会影响编译器,也即在编译阶段时候系统就会对有inline关键字的这种内联函数进行处理,系统会尝试将调用该函数的动作替换为函数的本体。通过这种方式来提高性能!
比如:
inline bool isEmpty(){//内联函数
return true;
}
//main.cpp中调用该内联函数
bool result = isEmpty();//这是正常调用函数的语句
当编译器识别到这是一个内联函数时,会直接对这个函数do处理,把上述语句变成:
bool result = true;//用 return的 true 这个函数本体来替换isEmpty()这个函数的调用语句
这样do之后,在你的程序运行时就不需要对这个"小"函数do一些压栈出栈的操作了,继而提高代码效率!
2)inline关键字仅仅可以让我们这些Cpp开发者向编译器提供这样上述作用一这样一个建议而已,编译器可以尝试去做,也可以不做,这将取决于编译器自身的诊断功能,也即:对于"小"函数的调用,将其函数的调用语句替换为函数的返回值本体这样一个功能它是否实现的决定权在编译器手上,我们程序员是无法控制的!
3)内联函数的定义是必须要放在.h头文件中的!
(why?因为,把内联函数的定义都放在.h头文件中时,当其他.cpp文件需要用到这个inline内联函数时,可以直接#include"inline函数的定义所在的头文件",把这个内联函数包含到当前要用到这个函数的.cpp源文件中去,以便于我对这个内联函数在编译时do预处理,把#include进来的函数本体的源代码替换掉函数调用的语句!你若是连这个函数体内有啥都不知道,你咋inline?你咋做替换呢?)
(不多bb,拿代码举例子)
//head1.h头文件 中的代码
#ifndef __HEAD1__H__
#define __HEAD1__H__
void func(int a, int t = 1) {
return;
}
#endif
//head1.cpp源文件 中的代码
#include"head1.h"//包含了head1.h
//main.cpp源文件 中的代码
#include<iostream>
#include"head1.h"//包含了head1.h
using namespace std;
int main(void){
return 0;
}
此时程序的运行结果为:
编译器会认为这个函数定义已经重复定义了,在head1.cpp 和 main.cpp中都定义了一遍,这当然会报错!所以,当我们使用inline关键字来修饰时,程序就可以0 error 0 warning了!
//修改head1.h头文件 中的代码
#ifndef __HEAD1__H__
#define __HEAD1__H__
inline void func(int a, int t = 1) {//在普通函数的返回值类型前面加上inline关键字
return;
}
#endif
这样程序就可以正常运行了!
4)inline的优缺点:
由于inline关键字会将函数的调用语句替换为函数本体(的代码通常比函数调用的多一点点),因此不可避免地会带来代码膨胀的问题,所以对于要被用做是inline内联的函数,其函数体必须要尽量小!也即你的inline的函数的函数体的代码要尽量少。
注意①:循环、分支、递归调用等代码尽量不要出现在inline函数中,否则你的inline函数在编译时会给编译器用这一大堆的函数体代码直接去替换你调用该函数的语句(因为这样do呢某些编译器就会因为这样拒绝让你的inline函数成为一个真正的inline函数),这样就会造成得不偿失的结局,本来你是想着提升效率的,现在这样效率没提升多少,代码却臃肿起来了。
constexpr函数可以认为是一种更加严格的inline函数
#define宏展开也类似于inline
注意②:在类/结构体内部定义的all的函数,都是inline内敛函数,只不过是隐式的内敛而已,当然你想显式的内敛,直接加上inline关键字即可!
三、函数杂合用法总结
(回顾一些函数的常见细节用法)
1)一个返回值类型为void的函数,可以给另外一个返回值类型也为void的函数调用甚至是return
废话不多说,直接上代码:
void ff1() {
cout << "this is ff1() !" << endl;
}
void ff2() {
cout << "this is ff2() !" << endl;
return ff1();//正确!调用返回值类型为void的函数ff1()
//return void;//错误!void不可以do返回值
//return;//正确!<==> 相当于啥都没return
}
//在main.cpp中调用ff2()
int main(void){
ff2();
return 0;
}
运行结果:
2)函数的返回值是指针or引用的2种case
函数返回值为引用的case:
废话不多说,直接上代码:
#include<iostream>
using namespace std;
//定义一个Person类
class Person {
public:
int m_Age;
string m_Name;
public:
Person():m_Age(0), m_Name(" "){}
Person(int age, string name) :m_Age(age), m_Name(name) {}
//重载=号
//这里直接返回一个引用,这样会符合链式编程的思想
//也即可以连续使用该函数do事情的意思,否则的话你只能一个语句用一次这个函数
//就想这样:(这样就不符合我们平时用a = b = c;这样的连续给赋值的习惯了!)
/*
p1 = p3;
p2 = p3;
*/
Person& operator=(const Person& p) {
this->m_Age = p.m_Age;
this->m_Name = p.m_Name;
return *this;
}
};
int main(void){
Person p1(21, "lyf");
Person p2(22, "lzf");
Person p3(23, "tjr");
cout << "p1.age = " << p1.m_Age << " p1.name = " << p1.m_Name << endl;
cout << "p2.age = " << p2.m_Age << " p2.name = " << p2.m_Name << endl;
cout << "p3.age = " << p3.m_Age << " p3.name = " << p3.m_Name << endl;
p1 = p2 = p3;//把p3的内容链式赋值给p1和p2
cout << "-----------------------------------" << endl;
cout << "p1.age = " << p1.m_Age << " p1.name = " << p1.m_Name << endl;
cout << "p2.age = " << p2.m_Age << " p2.name = " << p2.m_Name << endl;
cout << "p3.age = " << p3.m_Age << " p3.name = " << p3.m_Name << endl;
system("pause");
return 0;
}
运行结果:
当然,存在一种不安全的返回引用类型的函数写法:
直接上代码:
//把函数体内的局部变量的引用返回回去
int& fff() {
int tempInt = 10;
cout << "&tempInt = " << &tempInt << endl;
return tempInt;
}
//main.cpp对于fff()的调用 1 ---这样调用是不安全的!!!
int main(void){
int& k = fff();
//不安全,因为用int& 引用类型的变量k接受fff()的返回值时,k的地址和原fff函数体内的局部变量
//tempInt的地址就是一样的了,这样你又陷入到,对你没权限的地址空间do事情这样一种不安全的境地了!
cout << "&k = " << &k << endl;
return 0;
}
//main.cpp对于fff()的调用 2 ---这样调用是安全的!!!
int main(void){
int k = fff();//安全,因为在栈区重新申请了一块地址空间来存放k这个变量
cout << "&k = " << &k << endl;
//k和tempInt的地址不一样!so 安全,你是对一块你有使用权限的地址空间do事情!okk
return 0;
}
调用1的运行结果:
调用2的运行结果:
综上,相信你已经初步认识到,一般都是用在类当中让一个成员函数返回&引用类型。
函数返回值为指针的case:(千万不能这么干!)
int* reviseAddressNum()
{
int p = 6;
return &p;//返回该临时变量的地址
//这是不可以的,因为当函数执行完毕之后,p这个临时变量所占据的内存空间已经被系统回收了
//你既不能读也不能写了,也即你不能够再使用它的意思,用了就给你warning甚至是让你的程序崩溃!
}
//再在main.cpp中给这个地址写入数字
int main(void){
int * p = reviseAddressNum();
*p = 666;
return 0;
}
运行结果:
注意:函数中的局部变量的生存周期是这个函数的整个的调用期间,当该函数调用完毕后其函数内的局部变量的地址就会给自动释放掉!也即在调用完该函数后,对于该函数内的all的局部变量它们都没有权限再对其原来的地址空间do事情!因为这一块内存空间已经不属于这个局部变量了!你再这样强硬的返回这个地址并对它do事情的话,轻则就是编译器给你来一个warning,重则是整个程序都崩掉!(so以后coding必须要注意这个点,不能这么干!)
3)当函数没有形参列表param时(也即形参表为空时),要写做 :
returnTpye Function_Name(void){...}
4)对于不调用的函数,可以只给出这个函数的声明codes而没有定义codes
int a();//声明有a()这样一个函数
int b();//声明有b()这样一个函数
5)普通函数,其具体的函数实现只能有一个(放在.cpp源文件中),但是其函数原型声明可以有多次
//函数原型声明可以有多次!
int a();//√
int b();//√
int a();//√
int b();//√
//但是函数的定义(具体函数实现)只能有1次!
//a.cpp
int a(){ return 1;}
//b.cpp
int b(){ return 2;}
6)在Cpp中,提倡用引用&类型来取代指针类型的形参。
(老师告诉我,大量源码都是在这样写的)
void reviseInt(int& ta,int& tb){
ta = 10;
tb = 20;
}
这样do可以避免传参时让编译器为你的参数直接进行拷贝的操作,这样一旦遇到比较大的参数比如Person类型这样的自定义的占据内存比较大的参数时,进行拷贝就会大大降低效率!而传引用的话就会给这个传入的参数起个别名(别名和原变量/对象的地址一样),这样既可以修改外界变量,也可也达到提升效率性能的目的!可谓是一举两得!
7)函数重载(同名函数的重载)需要有3大条件:
(其实还有一种特殊的函数重载条件,只不过是在类中才能使用)
①形参类型不同
//比如:
void f(int a){}
void f(double a){}
②形参个数不同
//比如:
void f(int a,int b){}
void f(int a){}
③形参出现的顺序不同
//比如:
void f(int a,double b){}
void f(double b,int a){}
注意:对于类中的成员函数而言,在函数参数表后加上const可以让成员函数得以重载!
(注意wor,不是把const加在函数的形参表中的哟,这样是不能重载函数的!)
class Person {
public:
int m_Age;
string m_Name;
public:
Person() :m_Age(0), m_Name(" ") {}
void a() {
cout << "I'm the normal func" << endl;
}
void a()const {
cout << "I'm the const func" << endl;
}
};
//main.cpp
int main(void){
const Person p1;
Person p2;
p1.a();//调用的是const的函数a()
p2.a();//调用的是normal的函数a()
return 0;
}
运行结果:
四、const char* 、char const* 、char * const 三者的区别
(本质上就是看常量指针和指针常量这2个小知识点你是否学明白了)
1)const char* (常量指针)
char str[] = "I Love China!";
const char* pst = str;
//常量的字符指针(const char*),指针所指向的内容是不能(通过这个常量指针pst来)修改的!
//但是指针指向的地址空间是可以修改的!
*pst = 'Y';//×错误!其所指向的内容是不能修改的!
pst++;//√,pst++是指针所指向的地址后移的意思,这样没问题啊
str[0] = 'Y';//√,没有通过pst这个常量指针进行修改,这是very normal OK的!
2)char const *int (也是常量指针)
(本质上char const* <==> const char*)
(类比const int N = 10;<==> int const N = 10; | const char *p <==> char const *p
相信这样你就能理解why这样看了!)
char str[] = "I Love China!";
char const* pst2 = str;
*pst2 = 'Y';//×,因为常量指针所指向地址空间的内容不可通过该指针很修改的!
pst2++;//√
str[0] = 'Y';//√
3)char * const (指针常量)
char* const pst3 = str;//对于指针常量,顾名思义:指针(地址值)是个常量
//因此pst3这个指针所指向的地址空间(地址值)是const的,常量的,不不能通过pst3去修改这个地址值!
//但是该指针所指向的地址空间的值不是const的,因此你可以通过pst3去修改这个地址所指向的内容的值!
*pst3 = 'Y';//√
pst3++;//×,指针常量其地址值是const的!
str[0] = 'Y';//√
char* const pst3;
//×!因为指针常量在定义时就必须给它初始化!
//这就是定义引用类型时必须初始化且仅能初始化一次的原理:你再一次初始化时会把这个指针常量的地址值给修改了,这样肯定是不行的!违背了指针常量的含义!
注意:对于指针常量而言,定义它时就必须初始化(这是不是和引用类型一毛一样呢?)。是的,引用类型的本质其实就是一个指针常量,只不过要是每一次写代码时都写个指针常量非常有可能会出错,可读性差且易出错!因此,诞生了引用这样一种(我们称之为别名)的数据类型了!
补充)const char* const(常量 指针常量)
(指向的地址值和地址空间所存内容的值都不可以通过该变量进行修改)
const char * const pst4 = str;//这是 常量 指针常量
<==>
//char const * const pst4 = str;
//pst4所指向的地址值不能通过pst4进行修改
//pst4所指向的地址空间所存的内容的值也不能通过pst4进行修改
*pst4 = 'Y';//×
pst4++;//×
str[0] = 'Y';//√
const int& i =100;
<===>
const int* const i = 100;
五、函数形参中带const(好的代码习惯,非常推荐使用)
(为了防止你在某个函数体内部修改了你传入的形参的值,因此我们在写不需要修改所传入的参数的值的函数的代码时,都用常量引用的方式)
class Person {
public:
int m_Age;
string m_Name;
public:
Person() {}
Person(int age, string name) :m_Age(age), m_Name(name) {}
//重载=号
Person& operator=(const Person& p) {
this->m_Age = p.m_Age;
this->m_Name = p.m_Name;
//为了防止你意外修改了传入的Person对象p的年龄age,用const!
//p.m_Age = 1100;//×!因为const引用不允许你修改传入的参数的值!
//也即传入参数的值处于 只读,不可写 这样一种状态!
return *this;
}
}
函数参数带const的好处总结:
①可以防止你无意中修改了形参值,导致实参值被无意修改了
②所传入的实参的类型可以变得更加的灵活
(也即:给形参加了const引用类型之后,你不仅可以传入普通的实参,你还可以传入const引用类型的实参)
void funcc(Person& p) {}
Person p1;
const Person& ppp = p1;
funcc(p1);//√
funcc(ppp);//×因为引用的形参必须是一个可以修改的左值,ppp <==> 常量指针常量,so...you know
funcc(ppp2);//√
-------------------------------
void funcc2(const Person& p) {}
Person p1;
const Person& ppp = p1;
Person& ppp2 = p1;
funcc(p1);//√
funcc(ppp);//√
funcc(ppp2);//√
再比如:(实参类型更加灵活的例子2)
void func(int& a){
return;
}
int a = 1;
int& b = a;
func(a);//√
func(b);//√
func(13);//× 因为引用的形参必须是一个可以修改的左值
------------------------
void func(const int& a){
return;
}
int a = 1;
int& b = a;
func(a);//√
func(b);//√
func(13);//√
好,那么以上就是这一2.6小节我所回顾的内容的学习笔记,希望你能读懂并且消化完,也希望自己能牢记这些小小的细节知识点,加油吧,我们都在coding的路上~