文章目录

一、缺省参数

1、1缺省参数的概念

1、2 缺省参数的分类

1、2、1 全部缺省

1、2、2 半缺省参数

二、引用

2、1 引用的概念

2、2 引用特征

2、3 引用的使用场景

2、3、1 引用做参数

2、3、2 常引用

2、3、3 引用做返回值

2、4 引用总结

三、内联函数

3、1 内联函数的引出

3、2 内联函数详解

3、2、1 内联函数的概念

3、2、2 内联函数的特性

四、auto关键字(C++11)

4、1 auto简介

4、2 auto的使用细节

4、2、1 auto与指针和引用结合起来使用

4、2、2在同一行定义多个变量

4、3 auto不能推导的场景


‍♂️作者:@Ggggggtm‍♂️

专栏:C++

标题:C++基础语法

❣️寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景❣️

本篇文章解C++基础入门——语法篇(上)来讲述C++基础语法。学玩本篇文章就可以写出简单的C++程序了。

一、缺省参数

1、1缺省参数的概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。具体我们可看如下例子:

void Func(int a = 0){ cout<

下面为运行结果。我们看到第一个Func函数并没有传参,就默认输出了缺省参数0。

1、2 缺省参数的分类

1、2、1 全部缺省

全部缺省就是我们把形参全部给出缺省值。具体例子如下:

void Func(int a = 10, int b = 20, int c = 30){ cout<<"a = "<

这种情况我们在调用函数时,可传参,也可选择不传参。但是当我们传的参数只有一个时,默认是从左往右赋值的。也就是先给到a,再给到b,最后给到c。那要是我们想只给a,c赋值且不给b赋值呢?这种情况是不被允许的。我们只能从左往右依次赋值。

1、2、2 半缺省参数

我们先看具体实例:

void Func(int a, int b = 10, int c = 20){ cout<<"a = "<

半缺省参数是由几点要注意的:

  1. 半缺省参数必须从右往左依次来给出缺省值,不能间隔着给;
  2. 如果函数的声明和定义分开,缺省值不能在函数声明和定义中同时出现;
  3. 如果函数的声明和定义分开,缺省值在函数的声明时给出即可;
  4. 缺省值必须是常量或者全局变量;
  5. C语言不支持(编译器不支持) 。

二、引用

2、1 引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

这里给大家举一个例子。我想每个人都有一个小名吧。比如,班级上有一个 高同学 ,我们通常喊他为 小高 。那高同学 和 小高是一个人吗?答案肯定是一个人。我们在这里可以把高同学想象成变量,小高 是高同学的引用,小高犯错等同于高同学犯错。

我们这里再看一下引用的用法。类型& 引用变量名(对象名) = 引用实体。

void Test(){ int a = 10; int& ra = a;//<====定义引用类型printf("%p\n", &a); printf("%p\n", &ra);}

我们这里看到他们地址是相同的,也就是指向的同一块数据。

注意:引用类型必须和引用实体同种类型的。

2、2 引用特征

在使用引用时,我们需注意以下几点:

  1. 引用在定义时必须初始化;
  2. 一个变量可以有多个引用;
  3. 引用一旦引用一个实体,再不能引用其他实体。

我们结合下面实例一起理解一下。

void TestRef(){ int a = 10; int b = 20; // int& ra; // 该条语句编译时会出错 int& ra = a; int& rra = a; rra = b;//把b的值赋给rra,不是b的别名 printf("%p %p %p\n", &a, &ra, &rra); }

2、3 引用的使用场景

2、3、1 引用做参数

引用做参数,虽然大部分情况指针都可完成,但是引用做参数还是比较方便且较为容易理解的,我们看一下实例:

void Swap(int& left, int& right){ int temp = left; left = right; right = temp;}

上述对两个值交换,我们不再使用指针,直接可以使用引用进行交换。引用做参数还有另一个优势,就是在数据量较大,且数据本身也较大的情况下,引用作为参数的小路就会有所提高。为什么呢?

以值作为参数,在传参期间,函数不会直接传递实参,而是传递实参的一份临时的拷贝,因此用值作为参数,效率是非常低下的,尤其是当参数非常大时,效率就更低。 我们可以统一下面代码进行比较就可看出。

#include struct A{ int a[10000]; };void TestFunc1(A a){}void TestFunc2(A& a){}void TestRefAndValue(){ A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;}

运行结果如下:

2、3、2 常引用

我们先看如下代码:

const int a = 10;int& ra = a;

大家感觉上述的代码有问题吗?答案是有问题的。a 是一个常数,只有读的权限,不能对其修改。而 ra 是对 a 的引用,这里相当于对 a 来说权限放大了。如果正确的话,ra 可以修改 a 的值,但事实上是不可以的。我们应在 ra 前加上const。

我们再来看一段代码:

double d = 12.34; int& rd = d;

上面的代码正确吗?似乎因并没有错。但好像也错了,也为类型不同。首先上述的代码是有错的。但根本原因并不是类型不同。根本原因在于中间生成了一个临时变量,而临时变量具有常性

如下代码就正确了:

 double d = 12.34; const int& rd = d;

2、3、3 引用做返回值

引用做返回值时,有会有一些坑。我们可以看如下例子:

int& Count(){ int n = 0; n++; // ... return n;}int main(){ int ret = Count(); printf("%d ", ret );return 0;}

表面上似乎并没有什么错误,但实际上已经相当于访问野指针了。Count函数中n出了该函数就销毁了,变量 n 的那段地址空间的使用权限被收回了。Count函数的栈帧也会销毁。有的编译器在栈桢销毁时会清楚数据,有的并不会。在这里可能侥幸打印出正确数据,但确实是错误的。

函数的传值返回和传引用返回有什么区别呢?我们先来看一下传值返回:

int test(){ static int a=0; return a;}int main(){ int& b=test(); return 0;}

我们知道,当a为局部变量时(也就是上述代码中的 a 不加 static ),传值返回时,会把 a 拷贝给一个临时变量,再把临时变量的值赋给 test() 函数。注意:此时的临时变量在main函数中。那要是上述情况呢?同样与局部变量 a 的情况一样,把 a 拷贝给一个临时变量,再把临时变量的值赋给 test() 函数。编译器并不会在这里判断 a 出了作用域是否销毁,传值返回统一把值拷贝给一个临时变量,再把临时变量的值赋给函数

我们上面了解到了临时变量具有常性,所以上述的代码是有误的。我们应该在引用 b 前加上 const 。

引用做返回值同样也会有效率问题。以返回值类型为值,在返回期间,函数不会直接者将变量本身直接返回,而是返回变量的一份临时的拷贝,因此用值返回值类型,效率是非常低下的,尤其是返回值类型非常大时,效率就更低。

通过下面的对比也可看出:

#include struct A{ int a[10000]; };A a;// 值返回A TestFunc1() { return a;}// 引用返回A& TestFunc2(){ return a;}void TestReturnByRefOrValue(){ // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl;}

运行结果如下:

2、4 引用总结

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。 我们通过如下代码对比可知:

int main(){ int a = 10;int& ra = a; ra = 20;int* pa = &a; *pa = 20; return 0}

我们看其汇编代码:

其次我们对比一下指针和引用的区别:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址;
  2. 引用在定义时必须初始化,指针没有要求;
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
  4. 没有NULL引用,但有NULL指针;
  5. 在sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占四个字节);
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
  7. 有多级指针,但是没有多级引用;
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
  9. 引用比指针使用起来相对更安全。

三、内联函数

3、1 内联函数的引出

我们知道函数的调用是有所消耗的,需要开辟栈帧。为了提高效率呢,我们有时候可以选择用宏函数来替代函数调用。例如下面代码就可以用宏函数替代:

//int add(int x, int y)//{//return (x + y) * 10;//}#define ADD(x,y) (((x)+(y))*10)int main(){//int ret = add(1, 2);//宏函数int ret = ADD(1, 2);cout << ret << endl;return 0;}

注意,在使用宏函数时,一定不要吝啬括号。如果少一些必要的括号的话,会出现预想不到的错误。因为宏是在编译时进行完全的替换。

但是宏能替换所有的函数吗?答案是不可能的。稍微复杂的函数,用宏来替换就十分复杂。我们再来看一下宏的优缺点,先看优点:

  • 增强代码的复用性;
  • 提高性能;

我们再来看一下宏的缺点:

  • 不方便调试宏。(因为预编译阶段进行了替换);
  • 导致代码可读性差,可维护性差,容易误用;
  • 没有类型安全的检查 。

宏的替换并不是很好,且在大多情况下用宏来替换的复杂度很高、可读性差。那还有什么方法呢?C++中引出了内联函数,我们接着往下看。

3、2 内联函数详解

3、2、1 内联函数的概念

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

3、2、2 内联函数的特性

内联函数有以下特性:

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现),不是递归、频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议:
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地 址了,链接就会找不到。
    // F.h#include using namespace std;inline void f(int i);// F.cpp#include "F.h"void f(int i){cout << i <四、auto关键字(C++11) 

    4、1 auto简介

    在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么? C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。 我们看auto的使用方法,如下:
    #includeusing namespace std;int TestAuto(){return 10;}int main(){int a = 10;auto b = a;auto c = 'a';auto d = TestAuto();cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化return 0;}

    运行结果如下:

    从这个用例中,我们可大致了解auto的用法及其作用,但是自动推出类型的意义大吗?上述并不能很好的体现出来,我们看下面的一个例子。

    #include #include int main(){std::map m{ { "apple", "苹果" }, { "orange", "橙子" },{"pear","梨"} };std::map::iterator it = m.begin();while (it != m.end()){//....}return 0;}

    通过上面的代码,我们可以看出来随着程序越来越复杂,程序中用到的类型也越来越复杂。含义不明确又很容易导致出错。我们第一反应是可以通过typedef给类型取别名,比如:

    #include #include typedef std::map Map;int main(){Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };Map::iterator it = m.begin();while (it != m.end()){//....}return 0;}

    但是auto从不能解决所有的情况。在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11就引出了auto。

    注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

    4、2 auto的使用细节

    4、2、1 auto与指针和引用结合起来使用

    用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。具体我们可看下面代码:

    int main(){int x = 10;auto a = &x;auto* b = &x;auto& c = x;//引用cout << typeid(a).name() << endl;cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;*a = 20;*b = 30;c = 40;return 0;}

    运行结果如下:

    4、2、2在同一行定义多个变量

    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。我们可结合下面代码一起理解一下:

    void TestAuto(){auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同}

    4、3 auto不能推导的场景

    以下几点都是auto不能推到的场景:

    • auto不能作为函数的参数;
      // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导void TestAuto(auto a){}
    • auto不能直接用来声明数组;
      void TestAuto(){ int a[] = {1,2,3}; auto b[] = {4,5,6};//错误}
    • 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法;
    • auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。