C++入门基础

本章内容:

一、C++前言

1. 什么是C++

C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想 ,支持面向对象的程序设计语言应运而生。1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计

2.C++的发展史

1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是**在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。**语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本。

阶段 内容
C with classes 类及派生类、公有和私有成员、类的构造析构、友元、内联函数、赋值运算符重载等
C++1.0 添加虚函数概念,函数和运算符重载,引用、常量等
C++2.0 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数
C++3.0 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理
C++98 C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库)
C++03 C++标准第二个版本,语言特性无大改变,主要∶修订错误、减少多异性
C++05 C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即∶计划在本世纪第一个10年的某个时间发布
C++11 增加了许多特性,使得C++更像一种新语言,比如∶正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等
C++14 对C++11的扩展,主要是修复C++11中漏洞以及改进,比如∶泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等
C++17 在C++11上做了一些小幅改进,增加了19个新特性,比如∶static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等

并且C++不止于此,还在持续的往后更新。

二、C++关键字

C++总计63个关键字:

C语言32个关键字:

三、命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化 ,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的.

不使用命名空间,存在命名污染问题:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
 printf("%d\n", rand);
 return 0;
}
// 编译后后报错:error C2365: "rand": 重定义;以前的定义是"函数"

使用命名空间,改善命名污染问题:

1. 命名空间定义

格式:namespace +命名空间的名字+{} (不需要加";")

{}中都为命名空间内部的成员,不展开或暴露或者直接访问该命名空间的话,该命名空间里面的变量/函数/类型 等等都只局限于该命名空间,外面看不到。不过也可以通过直接显示域作用符 或 展开命名空间 或暴露 变量/函数/类型等 调用。

  • 正常的命名空间定义
CPP 复制代码
namespace lx
{
    // 命名空间中可以定义变量/函数/类型
    int rand = 10;
    int Add(int left, int right)
    {
        return left + right;
    }
    struct Node
    {
        struct Node* next;
        int val;
    };
}
  • 命名空间可以嵌套定义
CPP 复制代码
namespace LX
{
    int a;
    int b;
    int Add(int left, int right)
    {
        return left + right;
    }
    namespace N2
    {
        int c;
        int d;
        int Sub(int left, int right)
        {
            return left - right;
        }
    }
}
  • 命名空间的合并
cpp 复制代码
//同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
// 注:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace Lx
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}
// test.cpp
namespace Lx
{
    int div(int left, int right)
    {
        return left * right;
    }
}


//合并之后
namespace Lx
{
    int Mul(int left, int right)
    {
        return left * right;
    }
    
    int div(int left, int right)
    {
        return left * right;
    }
}

2.命名空间的使用

  • 错误的使用方法:
cpp 复制代码
namespace bit
{
    // 命名空间中可以定义变量/函数/类型
    int a = 0;
    int b = 1;
    int Add(int left, int right)
    {
        return left + right;
    }
    struct Node
    {
        struct Node* next;
        int val;
    };
}
int main()
{
    // 编译报错:error C2065: "a": 未声明的标识符
    printf("%d\n", a);
    return 0;
}
  • 正确的使用方法:

2.1加命名空间名称及作用域限定符

符号"::"在C++中叫做作用域限定符,我们通过"命名空间名称::命名空间成员"便可以访问到命名空间中相应的成员。

格式:命名空间名称 ::命名空间成员

cpp 复制代码
#include <stdio.h>
namespace Lx
{
    int a=1;
    int b=3;
}
int main()
{
    LX::a=2;
    printf("%d\n", N::a);//stdout: 2
    return 0; 
}

2.2使用using将命名空间中某个成员引入

CPP 复制代码
#include <stdio.h>
namespace Lx
{
    int a=1;
    int b=3;
}
using Lx::a;

int main()
{
    b=4;
    printf("%d\n", b);//stdout: 4
    return 0; 
}

2.3**使用using namespace **命名空间名称 引入

CPP 复制代码
#include <stdio.h>
namespace Lx
{
    int a=1;
    int b=3;
}
using namespace Lx;

int main()
{
    a=5;
    b=6;
    printf("%d %d\n", a,b);//stdout: 5 6
    return 0; 
}

四、C++输入&输出

cpp 复制代码
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
 cout<<"Hello world!!!"<<endl;
 return 0;
}
  • C语言中输入输出,我们第一个学的printf("hello world\n");就是输出,scanf("%s",&name);就是输入。需要包含头文件stdio.h

  • 而C++中的输入是cin,输出是cout。cin和cout分别叫做标准输入标准输出。用法是cin>>a;(向a中写入)。cout<<a;(输出a的值)。需要包含的头文件是iostream,并且还需要包含命名空间std-即using namespace std;

C++的输入和输出相交于C语言来说,优点是C++的输入和输出不需要格式的控制,而是编译器自动识别格式。如整形%d,字符串%s等等。C++的输入和输出都可以让编译器自动识别。

说明:

  1. 使用cout 标准输出对象**(控制台 ) cin标准输入对象 (键盘 )时,必须包含< iostream >**头文件以及按命名空间使用方法使用std。

  2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。

  3. **<<是流插入运算符,>>**是流提取运算符。

  4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。

  5. 实际上cout和cin分别是ostream和istream类型的对象,>>和<<涉及运算符重载等知识。

展示:

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    int a;
    double b;
    char c;
    char str[20];
    cin>>a;//输入2
    cin>>b;//输入3.5
    cin>>c;//输入+
    cin>>str;//输入abcdef
    cout<<a<<endl;//输出2
    cout<<b<<endl;//输出3.5
    cout<<c<<endl;//输出+
    cout<<str<<endl;//输出abcdef
 	return 0;
}

注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用**+std**的方式

五、缺省参数

1.缺省参数概念

缺省参数是指在声明或定义函数 时,为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

CPP 复制代码
#include <iostream>
using namespace std;
void getA(int A= 0)
{
	cout << A << endl;
}
int main()
{
	getA();//没有指定实参,使用参数的默认值 stdout:0
	getA(5);//指定了实参,使用指定的实参 stdout:5
	return 0;
}

2.缺省参数分类

  • 全缺省参数
cpp 复制代码
void Func(int a = 10, int b = 20, int c = 30)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
  • 半缺省参数
CPP 复制代码
void Func(int a, int b = 10, int c = 20)
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}

注意:

  1. 半缺省参数必须从右往左依次来给出,且不能间隔着给
c 复制代码
void Func1(int a=5, int b, int c = 20)//错误,间隔着给了
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
void Func2(int a=20, int b = 10, int c)//错误,从左向右了
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
  1. 缺省参数不能在函数声明和定义中同时出现
cpp 复制代码
//a.h
void Func(int a = 10);
// a.cpp
void Func(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
  1. 缺省值必须是常量或者全局变量
cpp 复制代码
int N=5;
void Func3(int a, int b = N, int c=20)//正确
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}


const int n=10;
void Func4(int a, int b = n, int c=20)//正确
{
    cout<<"a = "<<a<<endl;
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
}
  1. C语言不支持(编译器不支持)

六、函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。

比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是"谁也赢不了!",后者是"谁也赢不了!

1.函数重载的概念

函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表必须不同。函数重载常用来处理实现功能类似,而数据类型不同的问题。

  • 函数重载的必要条件:函数名相同。
  • 函数重载的情况:1.参数类型不同。2.参数个数不同。3.参数类型的顺序不同。----总之,就是形参列表必须是不同的
  • 注:若仅仅是返回值不同,是不能构成函数重载的。
CPP 复制代码
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
    cout << "int Add(int left, int right)" << endl;
    return left + right;
}
double Add(double left, double right)
{
    cout << "double Add(double left, double right)" << endl;
    return left + right;
}

// 2、参数个数不同
void f()
{
    cout << "f()" << endl;
}
void f(int a)
{
    cout << "f(int a)" << endl;
}

// 3、参数类型顺序不同
void f(int a, char b)
{
    cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
    cout << "f(char b, int a)" << endl;
}


int main()
{
    Add(10, 20);
    Add(10.1, 20.2);
    f();
    f(10);
    f(10, 'a');
    f('a', 10);
    return 0;
}

2.函数重载的原理(名字修饰)

为什么C++支持函数重载,而C语言不支持函数重载呢?

在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接

在.c文件编译预处理汇编完后生成的.o文件内有符号表,编译器根据符号表找到test.o文件在main.o文件中对应的函数。若没有找到对应的,则会报链接错误。

(符号表内部存储了函数修饰规则的修饰 :对应的地址。链接的时候做的事情就是去符号表里面找对应的修饰规则的修饰,然后映射对应的地址,就可以找到对应的地址。然后将两个地址链接。)

C语言编译器下,C语言:其中C语言的符号表修饰规则是,直接以函数名为标志。

Linux编译器下,C++(g++): 而C++中符号表的修饰规则是,以Z+函数长度+函数名+参数类型首字母为标志修饰的。

总结:

1.C++函数重载的原理,就是在形成符号表的时候,符号表的修饰规则是Z+函数长度+函数名+参数类型首字母为标志修饰。在函数名相同的情况下,只要参数的顺序 或者 个数 或者类型有变化,都会引起符号表里面对函数标识的不同,链接时链接的定义也不同。导致了可以存在两个函数名相同的函数,因为本质的标识是在符号表的标识,而不是函数名的标识。

2.C语言不能函数重载的原因,是因为不具有C++符号表的修饰规则,C语言的符号表修饰规则就直接是函数名,所以,不能存在两个相同函数名的函数,否则会报重定义错误。

3.extern "C"

由于C和C++编译器对函数名字修饰规则的不同,在有些场景下可能就会出问题,比如

  1. C++中调用C语言实现的静态库或者动态库,反之亦然。

  2. 多人协同开发时,有些人擅长用C语言,有些人擅长用C++

在这种混合模式下开发,由于C和C++编译器对函数名字修饰规则不同,可能就会导致链接失败,在该种场景下,就需要使用extern "C"。在函数前加extern "C" ,意思是告诉编译器,将该函数按照C语言规则来编译。

c++ 复制代码
//test.h
#include <iostream>
using namespace std;

#ifdef __cplusplus //编译器自行宏定义的一个宏,用于判别是否是C++工程.此处,如果是C++工程,则有_cplusplus被定义,则会执行extern "C",表明括号内部是以C语言形式编译的代码。如果不是C++工程,则会跳过此处。
extern"C"
{
#endif
    int func1(int left, int right);
    int func2(int left, int right);
#ifdef __cplusplus 
}
#endif


//func.c
int func1(int left,int right)
{
    return left+right;
}
int func2(int left,int right)
{
    return left*right;
}

//main.c
//////////////////////////////////////////////////////////////////////////////////// 
//创建一个c++工程,使用上面程序编程工程的静态库
#include <iosrteam>
using name spacestd;

#pragma comment(lib, "./../Debug/CalcLib.lib")

int main()
{
    int ret1=func1(1,2);
    cout<<ret;
    int ret2=func2(2,3);
}

若上面的函数的声明没有用extern "C"来声明的话。test.h里面的函数声明是C++风格,而func.c里面的函数定义是C语言风格。两个文件在形成符号表时候的修饰规则不同,则会找不到双方来链接,会报链接错误(无法解析的外部命令)。

七、引用

1.引用概念

引用 不是新定义一个变量,而是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如:李逵,在家称为"铁牛",江湖上人称**"黑旋风"**。李逵,铁牛,黑旋风,都是指同一个人。

使用方式:类型& 引用变量名(对象名)=引用实体; 如:int b=&a; 此时b就是a,不论修改a还是修改b,两者都会发生变化。

cpp 复制代码
void func()
{
	int a=1;
	int& b=a;
	printf("%d",a++);//stdout 1
	printf("%d",b);//stdout 2
}

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

2.引用特性

  1. 引用在定义时必须初始化

  2. 一个变量可以有多个引用

  3. 引用一旦引用一个实体,再不能引用其他实体。

cpp 复制代码
void test()
{
	int a=10;
    int b=5;
    //引用要初始化
    //int& rb;//error
    
    //引用一个实体后,不能再引用其他实体
    //int &ra =b;//error
    
    //一个变量可以有多个引用
    //int &rb=a;int &ra=a;
    
    //正确方法
    int& ra=a;
    int& raa=ra;
    ra++;
    printf("%d %d %d",a,ra,raa);//stdout 11 11 11
    raa++;
    printf("%d %d %d",a,ra,raa);//stdout 12 12 12
}

3.常引用

先了解两个概念,权限放大 和 权限缩小。(const是安全的,是权限小的,被限制的。非const是不安全的,是权限大的,没有被限制的。)

  • 权限放大:用非const变量引用const变量。是不合法的。如:
cpp 复制代码
const int a=10;
int& b=a;//error
  • 权限缩小:用const变量引用非const变量。是合法的。如:
cpp 复制代码
int a=10;
const int& b=a;//正确的
  • 权限的平移
cpp 复制代码
//非const引用非const
int a=10;
int& ra=a;//正确的

//const引用const
const int b=10;
const int rb=b;//正确的

下面是常见的错误:

cpp 复制代码
voidTestConstRef()
{
    const int a=10;
    //int& ra = a;   // 该语句编译时会出错,a为常量,ra为非const,权限的放大
    const int&ra=a;
    // int& b = 10;  // 该语句编译时会出错,10为常量,b为非const,权限的放大
    const int&b=10;
    double d=12.34;
    //int& rd = d;  // 该语句编译时会出错,类型不同
    const int&rd=d;
}

4.使用场景

  • 做参数
cpp 复制代码
void func(int& a,int& b)
{
    return a+b;
}
  • 做返回值
cpp 复制代码
int& func()
{
    static int n=0;
    n++;
    //...
    return n;
}
  • 常见错误
CPP 复制代码
#include <iostream>
using namespace std;

//错误
int& func1(int a,int b)
{
    int n=a+b;
    return n;
}

//错误
int func2(int a,int b)
{
    int n=a+b;
    return n;
}

//正确
int func3(int a,int b)
{
    int n=a+b;
    return n;
}


int main()
{
    int& ret1=func1(1,2);
    func1(2,3);
    cout<<ret1<<endl;
    
    int& ret2=func2(1,2);
    func2(2,3);
    cout<<ret2<<endl;
    
    //正确
    int ret2=func2(1,2);
    func2(2,3);
    cout<<ret2<<endl;
    
    return 0;
}

错误:

  1. func1函数用了传引用返回,在出了函数之后,n就已经被销毁了,传引用返回的n对应的空间已经被系统给回收清理了或者刷新成随机值了,此时传引用返回的我们想要的n,其实已经不是我们的n了。会造成数据错误。且下一次再调用func1时,会重新生成一个n,和上次给ret传引用的随机空间不是同一个,所以下一次调用func1后,不会改变ret;
  2. func2函数用了传值返回,在出了函数之后,n先拷贝一份临时变量,这份临时变量是const类型的。然后在外部用int& ret的非const类型的ret来引用接收,属于是权限的放大了,是不合法的。且下一次再调用func1时,会重新生成一个n,和上次给ret传引用的随机空间不是同一个,所以下一次调用func1后,不会改变ret;当然假设是同一个的空间的话,这也是权限的放大,还是不合法的。

总结 :注意:如果函数返回时,出了函数作用域,如果返回==对象还在(还没还给系统)==,则可以使用==引用返回==,如果已经==还给系统了==,则必须使用==传值返回==。

5. 传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,如参数或者返回值是结构体或者类等非常大的时候,效率就更低。

  • 值和引用的作为参数类型的性能比较
cpp 复制代码
#include <iostream>
#include <time.h>
using namespace std;
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;
}
int main()
{
    TestRefAndValue();
    return 0;
}
  • 值和引用的作为返回值类型的性能比较
cpp 复制代码
#include <time.h>
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;
}
int main()
{
    TestReturnByRefOrValue();
    return 0;
}

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大

6.引用和指针的区别

  • 语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
cpp 复制代码
int main()
{
    int a = 10;
    int& ra = a;//语法上,只是给a取了个别名,没有开空间

    a++;
    cout<<"&a = "<<&a<<endl;//stdout 11
    a++;
    cout<<"&ra = "<<&ra<<endl;//stdout 12
    
    
    int b=10;
    int* pb=&b;//语法上,给pb开了空间(32位下开4字节,64位下开8字节),用于存储b的地址
    
    b++;
    cout<<"b = "<<b<<endl;//stdout 11
    b++;
    cout<<"*pb = "<<*pb<<endl;//stdout 12
    return 0;
}

底层实现上 实际是有空间的,因为引用是按照指针方式来实现的。

int& ra=a;

int* pb=b;

我们可以发现,指针和引用的底层是完全一样的。但语法上不一样,引用不需要开辟空间,只是起别名。指针需要开辟空间,存取变量的地址。

  • 引用和指针的不同点:
  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。

  2. 引用 在定义时必须初始化,指针没有要求

  3. 引用 在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

  4. 没有NULL引用,但有NULL指针

  5. 在sizeof 中含义不同**:引用 结果为引用类型的大小 ,但指针始终是地址空间所占字节个数**(32位平台下占4个字节)

  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

  7. 有多级指针,但是没有多级引用

  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

  9. 引用比指针使用起来相对更安全

注:指针和引用实现的本质都是一样的,都是传地址。只不过引用不需要开辟空间,而指针需要开辟空间。

八、内联函数

1. 概念

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

注:在release模式下,由于编译器优化程度过大,难以查看对应的汇编指令。在debug模式下,编译器优化程度仍然过高,我们要稍加修饰和操作才可查看。(并且debug需要配置才查看的到内联展开,且只在合适的时候展开--代码过长不展开,代码较短则展开)

测试代码:

cpp 复制代码
#include <iostream>
using namespace std;
int  Add1(int left, int right)
{
	return left + right;
}
inline int Add2(int left, int right)
{
	return left + right;
}
int main()
{

	cout << Add1(1, 2) << endl;
	cout << Add2(2, 3) << endl;

	return 0;
}
  • 普通函数调用
  • 内联函数调用

2. 特性

  1. inline是一种以空间换时间 的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。

如下,

不用内联函数则不会展开,调用一次函数要使用一次指令,那么调用1万次函数,则会用上1万次指令,加上被调用的函数100指令,总共就1W+100行。

若使用内联函数则可能展开,如果使用一万次这个函数,则他就会展开一万次,每次展开函数的指令都为100行,所以总共1W*100行,可以看到内联函数如果调用次数过多也是不好的。属于是以空间换时间了。

  1. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同 ,一般建议:将函数规模较小 (即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

  2. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

CPP 复制代码
// test.h
#include <iostream>
using namespace std;
inline int func1(int left,int right);


//test.c
int func1(int left,int right)
{
    return left+right;
}

//main.c
#include "test.h"
int main()
{
    cout<<func1(1,2)<<endl;//error,会链接阶段报链接错误
    return 0;
}

3.宏、函数、内联函数

  • 宏与函数优缺点
属性 #define定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果(如:a++;b--;这些在使用后会改变自身的,就是副作用参数) 函数参数只在传参的时候求值一次,结果更容易控制
参数类型 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的
可复用性 较好
可维护性
  • 内联函数

既弥补了宏的缺点,又兼具了宏的优点。所以C++中可以用内联函数来代替宏。内联函数只适用于短小代码,长代码其不会展开为内联,因为代码展开次数过多会导致程序的代码过长,也会拖慢程序。

  1. 常量定义换用const enum

  2. 短小函数定义换用内联函数

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

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型难于拼写

  2. 含义不明确导致容易出错

cpp 复制代码
#include <string>
#include <map>
int main()
{
    std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" }, 
                                         {"pear","梨"} };
    std::map<std::string, std::string>::iterator it = m.begin();
    while (it != m.end())
    {
        //....
    }
    return 0;
}
  • 解决方法一:typedef
cpp 复制代码
#include <string>
#include <map>
typedef std::map<std::string, std::string> Map;
int main()
{
 Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
 Map::iterator it = m.begin();
 while (it != m.end())
 {
 //....
 }
 return 0;
}

存在问题:在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型,而系统内部的typedef我们是比较难以知道的。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。

  • 解决方法二:auto关键字
cpp 复制代码
#include <string>
#include <map>
typedef std::map<std::string, std::string> Map;
int main()
{
 auto m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };//会自动根据类型进行推导
 auto it = m.begin();//会自动根据类型进行推导
 while (it != m.end())
 {
 //....
 }
 return 0;
}

1. auto简介

在早期的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。  在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

cpp 复制代码
float TestAuto()
{
 return 3.14;
}
int main()
{
 int a = 10;
 auto b = a;
 auto c = 'a';
 auto d = TestAuto();
 
 //typeid()是一个存储有传入参数类型的对象
 //typeid().name()可以获取传入类型参数的类型,通常用于识别参数类型
 cout << typeid(b).name() << endl;//stdout int
 cout << typeid(c).name() << endl;//stdout char
 cout << typeid(d).name() << endl;//stdout float
 
 //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
 return 0;
}

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

2.auto的使用细则

  1. auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&,否则auto出来的变量就是普通变量,而不是引用的变量。

CPP 复制代码
int main()
{
 int x = 10;
 auto a = &x;
 auto* b = &x;
 auto& c = x;
 cout << typeid(a).name() << endl;//stdout int*
 cout << typeid(b).name() << endl;//stdout int*
 cout << typeid(c).name() << endl;//stdout int
 *a = 20;
 *b = 30;
 c = 40;
 return 0;
}
  1. 在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

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

3.auto不能推导的场景

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

十、基于范围的for循环(C++11)

1. 范围for的语法

在C++98中如果要遍历一个数组,可以按照以下方式进行

CPP 复制代码
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	//[]法 将数组元素值全部乘以2
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		arr[i] *= 2;
	}
	//指针法 打印数组中的所有元素
	for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]);  ++p)
        cout << *p << endl;

	cout << endl;

以上这些方法都可以遍历一个数组,那么还有更简单的方法吗?有,那就是范围for

对于一个有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

CPP 复制代码
void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    //范围for 将数组元素都乘以2
    for(auto& e : array)//这里的e因为是引用变量,所以每次的e就是数组里元素的别名,如果e不是引用变量的话,则无法用范围for修改数组的元素
        e *= 2;

    //范围for 打印数组
    for(auto e : array)
        cout << e << " ";

    return 0;
}

注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环

2.范围for的使用条件

  1. for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

注意:以下代码就有问题,因为for的范围不确定

CPP 复制代码
void TestFor(int array[])//由于传进来的是数组,但是接收的实际上是指针,指针只能够知道传进来数组的起始地址,但是不能知道传进来数组的大小
{
    for(auto& e : array)
        cout<< e <<endl;
}
  1. 迭代的对象要实现++和==的操作。因为范围for底层就是封装了++和--。内置类型对象和部分已有自定义类型对象编译器语言已经提供了++和--。如果是自己实现的对象,要使用范围for,就得自己先实现该对象的++和--。

十一、指针空值nullptr(C++11)

1. C++98中的指针空值

在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:

cpp 复制代码
void TestPtr()
{
    int* p1 = NULL;//初始化,NULL的值其实就是0
    int* p2 = 0;

    // ......
}

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

cpp 复制代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

cpp 复制代码
void f(int)
{
    cout<<"f(int)"<<endl;
}
void f(int*)
{
    cout<<"f(int*)"<<endl;
}
int main()
{
    f(0);
    f(NULL);
    f((int*)NULL);
    return 0;
}

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,而调用的是fun(int)函数了,因此与程序的初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

NULL:

C语言下被定义为值0,C++下被定义为(void*)0。函数重载的时候可能会出现问题。

nullptr:

将C语言中(void*)0的NULL单独拎出来变成了nullptr;,只是(void *)0类型,函数重载的时候不会出现问题。

注意:

1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的

2. *在C++11中,sizeof(nullptr)与sizeof((void )0)所占的字节数相同。

3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

相关推荐
全栈派森7 分钟前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse12 分钟前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭1 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架1 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱1 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜2 小时前
Flask框架搭建
后端·python·flask
进击的雷神2 小时前
Perl语言深度考查:从文本处理到正则表达式的全面掌握
开发语言·后端·scala
进击的雷神2 小时前
Perl测试起步:从零到精通的完整指南
开发语言·后端·scala
豌豆花下猫3 小时前
Python 潮流周刊#102:微软裁员 Faster CPython 团队(摘要)
后端·python·ai
秋野酱3 小时前
基于javaweb的SpringBoot驾校预约学习系统设计与实现(源码+文档+部署讲解)
spring boot·后端·学习