【C++初阶:入门总结】命名空间 | 缺省参数 | 函数重载 | 引用 | 内联函数

简介: 【C++初阶:入门总结】命名空间 | 缺省参数 | 函数重载 | 引用 | 内联函数

文章目录

【写在前面】

点到为止,并不深入。其次建工程这里就不说了,在之前的基础上 —— 文件名.cpp 就可以了

一、C++关键字 (C++98)

💨 C 语言有 32 个关键字,而 C++ 有 63 个关键字,C 语言的关键字在 C++ 中继续可以使用

    ps:在本章中不对关键字进行详讲

❗ I/O ❕

//C++兼容C绝大多数语法 
#include<stdio.h>
int main01()
{
  printf("Hello CPLUSPLUS\n");
  return 0;
}
//但C++通常会这样写
#include<iostream>
using namespace std;
int main()
{
  cout << "Hello CPLUSPLUS" << endl;
  return 0;
}

二、命名空间

对比 C 语言,一般 C++ 每增加一个语法都是为了解决一些 C 语言做不到的事或者是 C 语言做的不好的地方

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

❗ C语言命名冲突示例 ❕

在不同的作用域中,可以定义同名的变量;在同一作用域下,不能定义同名的变量

#include<stdio.h>
//#include<stdlib.h>
int a = 0;
int rand = 10;
int main()
{
  int a = 1;
  printf("%d\n", rand);
  return 0;
}

📝 分析:

可以看到我们定义了一个 rand 变量输出是没有问题的,但如果包含了 stdlib 头时就会产生命名冲突,此时我们的 rand 变量就和库里的产生冲突;

实际除此之外在大型项目开发时,还有可能和同事之间发生冲突。C 语言面对这种问题是无法解决的,而对于这种场景 C++ 使用了命名空间

💦 命名空间定义

定义命名空间,需要使用到 namespace 关键字,后面跟命名空间的名字,然后接一对 {} 即可,{} 中即为命名空间的成员

⚠ 注意一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

//1. 普通的命名空间
namespace N1 // N1为命名空间的名称
{
  //命名空间中的内容,既可以定义变量,也可以定义函数
  int a;
  int Add(int left, int right)
  {
    return left + right;
  }
}
//2. 命名空间可以嵌套
namespace N2
{
   int a;
   int b;
   int Add(int left, int right)
   {
     return left + right;
   }
  namespace N3
  {
     int c;
     int d;
    int Sub(int left, int right)
    {
       return left - right;
    }
  }
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
namespace N1
{
  int a = 10;
}
namespace N1
{
  int b = 20;
}

❗ 用命名空间解决变量 rand 和 stdlib 库里的命名冲突 ❕

#include<stdio.h>
#include<stdlib.h>
namespace WD//定义了一个命名空间域
{
  int rand = 10;//定义变量
  int Add(int x, int y)//定义函数
  {
    return x + y; 
  }
  struct Node//定义结构体类型
  {
    struct Node* next;
    int val;  
  };
}
int main()
{
  printf("%p\n", rand);//函数指针
  printf("%d\n", WD::rand);//rand变量;‘::’叫做域作用限定符
  WD::Add(3, 5);//调用函数
  struct WD::Node node1;//结构体
  return 0;
}

❗ 嵌套命名空间 ❕

#include<stdio.h>
#include<stdlib.h>
namespace WD
{
  int w = 20;
  int h = 10;
  namespace WH//嵌套命名空间域
  {
    int w = 10;
    int h = 20;
  }
}
int main()
{
  printf("%d\n", WD::WH::h);//访问嵌套命名空间
  return 0;
}

❗ 相同名称的命名空间 ❕

namespace WD
{
  int a = 10;
  int b = 20;
  namespace WH
  {
    int a = 20;
    int b = 10;
  }
}
namespace WD//相同名称的命名空间
{
  int rand = 50;
  //int a = 10;//err,在合并的时候冲突了
}

💦 命名空间使用

❓ 如何使用命名空间里的东西 ❔

1️⃣ 全部直接展开到全局

using namespace WD;
//using namespace std;//std是包含C++标准库的命名空间

  💨优点:用起来方便

  💨缺点:自己定义的东西也会暴露,导致命名污染

2️⃣ 访问每个命名空间中的东西时,指定命名空间

std::rand;

  💨优点:不存在命名污染

  💨缺点:如果要去访问多个命名空间里的东西时,需要一一指定

3️⃣ 仅展开常用的内容

using WD::Node;
using WD::Add;

  💨优点:不会造成大面积的污染;把常用的展开后,也不需要一一指定


namespace WD
{
  int a = 10;
  int b = 20;
  //...
}
int main()
{
  //using namespace WD;//1.展开WD空间所有内容
  //printf("%d\n", WD::a);//2.指定命名空间
  //using WD::a;//3.仅展开a
}

❓ 上面说了 C++ 把库里的东西都放到 std 这个域里了,那直接展开 std 不就行了或者包头 ❔

  注意

   1️⃣ #include <iostream>:展开定义

   2️⃣ using namespace std;:允许用

  所以两者缺一不可

三、C++中的I/O

❗ 说明 ❕

使用 cout 标准输出 (控制台) 和 cin 标准输入 (键盘) 时,必须包含 < iostream> 头文件以及 std 标准命名空间 (std是包含 C++ 标准库的命名空间)

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

#include<iostream>
using namespace std;
int main()
{
  cout<<"Hello world!!!"<<endl;
  return 0;
}

❗ 上面这种写法其实有一定的规范缺陷 ❕

C++ 为什么用一个库去包它的命名空间,就是怕我们定义的与库冲突。但是一句 using namespace std; 就把库展开了,那么定义 std 的优势就无了。所以在项目中比较规范的写法如下:

#include<iostream>
//展开常用————工程项目中常见的对命名空间的用法
using std::cout;
using std::endl;
int main()
{
  //只要是库里的都得指定std
  //std::cout << "Hello world!!!" << std::endl
  //但如果cout经常要用的话,就在上面单独展开
  cout << "Hello world!!!" << endl;
  return 0;
}

⚠ 注意,在日常做题时不需要像项目那样规范,可以直接全部展开 std

❗ cout && cin ❕

cout 和 cin 类似 C 语言的 printf 和 scanf,这里只是先了解下,因为对于 C 语言中的 I/O 是函数,而 C++ 是对象

#include<iostream>
using namespace std;
int main()
{
  int n;
  cin >> n;//cin可以理解为键盘;>>可以理解为输入运算符/流提取运算符(官方)
  int* a = (int*)malloc(sizeof(int) * n);
  for(int i = 0; i < n; ++i)
  {
    cin >> a[i];
  }
  for(int i = 0; i < n; ++i)
  {
    cout << a[i] << " ";//cout可以理解为控制台;<<可以理解为输出运算符/流插入运算符(官方)
  }
  cout << endl;//等价于count << '\n';
  return 0;
}

❗ 对于I/O,C++比C便捷 ❕

C++ 不需增加数据格式控制,比如:整形 – %d,字符 – %c,它会自动实别

#include<iostream>
using namespace std;
int main01()
{
  int n;
  cin >> n;
  double* a = (double*)malloc(sizeof(int) * n);
  for(int i = 0; i < n; ++i)
  {
    cin >> a[i];//它会自动实别
  }
  for(int i = 0; i < n; ++i)
  {
    cout << a[i] << " ";//它会自动实别
  }
  count << endl;
  return 0;
}
//挺爽的吧!!!
int main()
{
  char ch = 'C';
  int i = 10;
  int* p = &i;
  double d = 3.14;
  double b = 3.1415926;
  cout << ch << endl;
  cout << i << endl;
  cout << p << endl;
  cout << d << endl;
  cout << b << endl;//注意对于浮点数,C++最多输出5位,当然也可以指定输出多少位,但是相对指定的方式(比较麻烦)可以配合printf使用
  return 0;
}

❗ 当然C++也并不完美 ❕

#include<iostream>
using namespace std;
struct Student
{
  char name[10];
  int age;
};
int main()
{
  struct Student s = { "小三", 18 };
  //输出结构体信息。类似下面场景printf更好
  //对于这种情况C++中不用纠结用啥,用习惯自己的就好
  cout << "名字:" << s.name << " " << "年龄:" << s.age << endl;
  printf("名字:%s 年龄:%d\n", s.name, s.age);
  return 0;
}

四、缺省参数

💦 缺省参数概念

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

#include<iostream>
using namespace std;
void TestFunc(int a = 0)//参数缺省值
{
  cout << a << endl;
}
int main()
{
  TestFunc();//没有指定实参,使用缺省值
  TestFunc(10);//指定实参,使用实参
  return 0;
}

❗ 有什么用 ❕

比如在 C 语言中有个很苦恼的问题是写栈时,不知道要开多大的空间,之前我们是如果栈为空就先开 4 块空间,之后再以 2 倍走,如果我们明确知道要很大的空间,那么这样就只能一点一点的接近这块空间,就太 low 了。但如果我们使用缺省,明确知道不需要太大时就使用默认的空间大小,明确知道要很大时再传参

#include<iostream>
using namespace std;
namespace WD
{
  struct Stack
  {
    int* a;
    int size;
    int capacity; 
  };
}
using namespace WD;
void StackInit(struct Stack* ps)
{
  ps->a = NULL; 
  ps->capacity = 0;
  ps->size = 0;
}
void StackPush(struct Stack* ps, int x)
{
  if(ps->size == ps->capacity)
  {
    //ps->capacity *= 2;//err
    ps->capacity == 0 ? 4 : ps->capacity * 2;//这里就必须写一个三目
  }
}
void StackInitCpp1(struct Stack* ps, int defaultCP)
{
  ps->a = (int*)malloc(sizeof(int) * defaultCP);
  ps->capacity = 0;
  ps->size = defaultCP;
}
void StackInitCpp2(struct Stack* ps, int defaultCP = 4)//ok
{
  ps->a = (int*)malloc(sizeof(int) * defaultCP);
  ps->capacity = 0;
  ps->size = defaultCP;
}
int main()
{
  //假设明确知道这里至少需要100个数据到st1
  struct Stack st1; 
  StackInitCpp1(&st1, 100);
  //假设不知道st2里需要多少个数据 ———— 希望开小点
  struct Stack st2;  
  StackInitCpp2(&st1);//缺省
  return 0;
}

💦 缺省参数分类

❗ 全缺省参数 ❕

void TestFunc(int a = 10, int b = 20, int c = 30)
{
  cout << "a = " << a << endl;
  cout << "b = " << b << endl;
  cout << "c = " << c << endl;
  cout << endl;
}
int main()
{
  //非常灵活,
  TestFunc();
  TestFunc(1);
  TestFunc(1, 2);
  TestFunc(1, 2, 3);  
  //TestFunc(1, , 3);//err,注意它没办法实现b不传,只传a和b,也就是说编译器只能按照顺序传
  return 0;
}

⚠ 注意:

  1️⃣ 全缺省参数只支持顺序传参

❗ 半缺省参数 ❕

//void TestFunc(int a, int b = 10, /*int f, - err*/ int c = 20);//err,
void TestFunc(int a, int b = 10, /*int f, int x = y, -> err*/ int c = 20)
{
  cout << "a = " << a << endl;
  cout << "b = " << b << endl;
  cout << "c = " << c << endl;
  cout << endl;
}
int main()
{
  //TestFunc();//err,至少得传一个,这是根据形参有几个非半缺省参数确定的
  TestFunc(1);
  TestFunc(1, 2);
  TestFunc(1, 2, 3);  
  return 0;
}
//a.h
void TestFunc(int a = 10);
//a.cpp
void TestFunc(int a = 20)
{}

⚠ 注意:

  1️⃣ 半缺省参数必须从右往左依次来给出,且不能间隔着给

  2️⃣ 缺省参数不能在函数声明和定义中同时出现

  3️⃣ 缺省值必须是常量或者全局变量

  4️⃣ C 语言不支持缺省

五、函数重载

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

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

💦 函数重载概念

函数重载:是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表 (参数个数或类型或顺序) 必须不同,常用来处理实现功能类似数据类型不同的问题,显然这是 C 语言做不到的东西

//1.参数个数
int Add(int x)
{}
int Add(int x, int y)
{}
//2.参数类型
int Add(int x, int y)
{}
int Add(int x, double y)
{}
//3.参数顺序
int Add(int x, float y)
{}
int Add(float y, int x)
{}

❗ 怎么调用 ❕

int Add(int left, int right)
{
  return left + right;
}
double Add(double left, double right)
{
  return left + right;
}
long Add(long left, long right)
{
  return left + right;
}
int main()
{
  //这里分别调用三种不同的函数
  Add(10, 20);//默认整型
  Add(10.0, 20.0);//浮点型
  Add(10L, 20L);//长整型
  return 0;
}

⚠ 注意

对于函数重载,如果你想调用某一函数,那么在传参的时候就必须写好对应参数的类型、个数、顺序,比如 float 数据,就要写 3f,因为这样它才能找到对应的函数调用

💦 名字修饰 (name Mangling)

❓ 为什么C++支持函数重载,而C语言不支持函数重载呢,以 Linux 环境下演示 ❔

注意这里就不细讲 Linux 的一些指令了,具体的我都写在或者将写在 《Linux专栏》里了

先在 Linux 下以两种编译方式编译函数重载的程序,这里有三个文件 :f.h、f.c、test.c

  ▶ 可以看到以 C 语言去编译函数重载报错了

  ▶ 可以看到以 C++ 去编译函数重载是可以的

在正式探究前我们先回忆下,注意 C/C++ 都有类似的过程,但肯定是有区别的,现在我们要找的就是那个区别

  这里我们对照着程序走一遍过程

然后再回到我们的问题

  ❓ C语言不支持函数重载 ❔

   C 编译器,直接用函数名关联,函数名相同时,无法区分

  ❗ 验证 ❕

   通过命令 objdump -S 去查看 C 编译生成的符号表

   通过命令 objdump -S 去查看 C++ 编译生成的符号表

  ❓ C++ 支持函数重载 ❔

   从上就可以看出对于函数重载 C++ 相对于 C 语言引入了某种规则

   C++ 引用了《函数名修饰规则 (Linux下)》不能直接用函数名,要对函数名进行修饰 (带入参数的特点修饰)

    📝 说明:

     1️⃣ _Z 是前缀

     2️⃣ 3 是函数名的长度

     3️⃣ Add 是函数名

     4️⃣ ii / dd 是函数参数类型的首字母,如果是 int* i,那么就是 pi

💨小结:

  C++ 是支持函数重载的,函数名相同,只要参数不同,修饰在符号表中的名字也不同,那么就能区分了

❗ Windows下函数名修饰规则 ❕

🍳【扩展学习:C/C++函数调用约定和名字修饰规则】

C++函数重载

C/C++ 函数调用约定

❓ 下面两个函数属于函数重载吗 (编译器能不能只实现返回值不同,就能构成重载) ❔

short Add(short left, short right)
{
  return left+right;
}
int Add(short left, short right)
{
  return left+right;
}

显然《函数名修饰规则》并不能让它们支持重载。

 ❓ 其次如果想自己定义《函数名修饰规则》让只有返回值不同的函数支持重载可以吗 ❔

  💨 编译器层面是可以区分的

   short Add; -> _Z3sAdd

   int Add; -> _Z3iAdd

  💨 语法调用层面有严重歧义

   Add(3, 5); ???

❓ 下面两个函数能形成函数重载吗?❔

void TestFunc(int a = 10)
{
  cout<<"void TestFunc(int)"<<endl;
}
void TestFunc(int a)
{
  cout<<"void TestFunc(int)"<<endl;
}

虽然两个函数的参数是缺省值和非缺省值,但是依旧不影响修饰出来的函数名,所以不能构成函数重载

💦 extern"C"

有时候在 C++ 工程中可能需要将某些 (部分) 函数按照 C 的风格来编译,在函数前加 extern “C”,意思是告诉编译器,将该函数按照 C 语言规则来编译。比如:tcmalloc 是 google 用 C++ 实现的一个项目,他提供 tcmallc() 和 tcfree两个接口来使用,但如果是 C 项目就没办法使用,那么他就使用 extern “C” 来解决。

extern "C" int Add(int left, int right);
int main()
{
  Add(1,2);
  return 0; 
}

💨总结

  1️⃣ C++ 项目可以调用 C++ 库,也可以调用 C 的库,C++ 是直接兼容 C 的

  2️⃣ C 项目可以调用 C 库,也可以使用 extern"C" 调用 C++ 库 (C++ 提供的函数加上 extern"C")

六、引用

💦 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,语法理解上程序不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"

❗ 类型& 引用变量名(对象名) = 引用实体 ❕

int main()
{
  //有一块空间a,后面给a取了三个别名b、c、d
  int a = 10;
  int& b = a;
  int& c = a;
  int& d = b;
  //char& d = a;//err,引用类型和引用实体不是同类型(这里有争议————char a = b[int类型],留个悬念,下面会解答)
  //会被修改
  c = 20;
  d = 30;
  return 0;
}

⚠ 注意

 1️⃣ 引用类型必须和引用实体是同种类型

 2️⃣ 注意区分 ‘&’ 取地址符号

💦 引用特性

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

 2️⃣ 一个变量可以有多个引用

 3️⃣ 引用一旦引用一个实体,再不能引用其他实体

int main()
{
  //int& e;//err
  int a = 10;
  int& b = a;
  //这里指的是把c的值赋值于b
  int c = 20;
  b = c;
  return 0;
}

💦 常引用

void TestConstRef()
{
  const int a = 10;
  //int& ra = a; //该语句编译时会出错,a为常量;由const int到int
  const int& ra = a;//ok
  int b = 20;
  const int& c = b; //ok,由int到const int
  //b可以改,c只能读不能写
  b = 30;
  //c = 30;//err
  //b、c分别起的别名的权限可以是不变或缩小
  int& d = b;//ok
  //int& e = c//err
  const int& e = c;//ok
  //int& f = 10; // 该语句编译时会出错,b为常量
  const int& g = 10;//ok
  int h = 10;
  double i = h;//ok
  //double& j = h;//err
  const double& j = h;//ok
  //?为啥h能赋值给i了(隐式类型转换),而给h起一个double类型的别名却不行————如果是仅仅是类型的问题那为啥加上const就行了?
  //double i = h;并不是直接把h给i,而是在它们中间产生了一个临时变量(double类型、常量),并利用这个临时变量赋值
  //也就是说const double& j = h;就意味着j不是直接变成h的别名,而是变成临时变量(doublde类型)的别名,但是这个临时变量是一个常量,这也解释了为啥需要加上const
}

💨小结

 1️⃣ 我能否满足你变成别名的条件:可以不变或者缩小你读写的权限 (const int -> const int 或 int -> const int),而不能放大你读写的权限 (const int -> int)

 2️⃣ 别名的意义可以改变,并不是每个别名都跟原名有一样的权限

 3️⃣ 不能给类型不同的变量起别名的真正原因不是类型不同,而是隐式类型转换后具有常性了

❗ 常引用的意义 (举例栈) ❕

typedef struct Stack
{
  int* a;
  int top;
  int capacity;
}ST;
void InitStack(ST& s)//传引用是为了形参的改变影响实参
{//...}
void PrintStack(const ST& s)//1.传引用是为了减少拷贝 2. 同时保护实参不会被修改
{//...}
void Test(const int& n)//即可以接收变量,也可以接收常量
{//...}
int main()
{
  ST st;
  InitStack(st);
  //...
  PrintStack(st);
  int i = 10;
  Test(i);
  Test(20);
  return 0;
}

💨小结

 1️⃣ 函数传参如果想减少拷贝使用引用传参,如果函数中不改变这个参数最好使用 const 引用传参

 2️⃣ const 引用的好处是保护实参,避免被误改,且它可以传普通对象也可以传 const 对象

💦 使用场景

❗ 1、做参数 ❕

void Swap1(int* p1, int* p2)
{
  int temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}
void Swap2(int& rx, int& ry)
{
  int temp = rx;
  rx = ry;
  ry = temp;
}
int main()
{
  int x = 3, y = 5;
  Swap1(&x, &y);//C传参
  Swap2(x, y);//C++传参
  return 0;
}

💨在 C++ 中形参变量的改变,要影响实参,可以用指针或者引用解决

意义:指针实现单链表尾插 || 引用实现单链表尾插

  指针

  引用

void SListPushBack(SLTNode*& phead, int x)
{
  //这里phead的改变就是plist的改变
}
void TestSList2()
{
  SLTNode* plist = NULL;
  SListPushBack(plist, 1);
  SListPushBack(plist, 2);
}

  有些书上喜欢这样写 (不推荐)

typedef int SLTDataType;
typedef struct SListNode
{
  SLTDataType data;
  struct SListNode* next;
}SLTNode, *PSLTNode;
void SListPushBack(PSLTNode& phead, int x)
{
  //...
}

❗ 2、做返回值 ❕

 2.1、传值返回

//传值返回
int Add(int a, int b)
{
  int c = a + b;
  return c;//需要拷贝
}
int main()
{
  int ret = Add(1, 2);//ok, 3
  Add(3, 4);
  cout << "Add(1, 2) is :"<< ret <<endl;
  return 0;
}

💨Add 函数里的 return c; —— 传值返回,临时变量作返回值。如果比较小,通常是寄存器;如果比较大,会在 main 函数里开辟一块临时空间

  怎么证明呢

int Add(int a, int b)
{
  int c = a + b;
  return c;
}
int main()
{
  //int& ret = Add(1, 2);//err
  const int& ret = Add(1, 2);//ok, 3
  Add(3, 4);
  cout << "Add(1, 2) is :"<< ret <<endl;
  return 0;
}

💨从上面就可以验证 Add 函数的返回值是先存储在临时空间里的

 2.2、传引用返回

//传引用返回
int& Add(int a, int b)
{
  int c = a + b;
  return c;//不需要拷贝
}
int main()
{
  int ret = Add(1, 2);//err, 3
  Add(3, 4);
  cout << "Add(1, 2) is :"<< ret <<endl;
  return 0;
}

💨结果是不确定的,因为 Add 函数的返回值是 c 的别名,所以在赋给 ret 前,c 的值到底是 3 还是随机值,跟平台有关系 (具体是平台销毁栈帧时是否会清理栈帧空间),所以这里的这种写法本身就是越界的 (越界抽查不一定报错)、错误的

  发现这样也能跑,但诡异的是为啥 ret 是 7

//传引用返回
int& Add(int a, int b)
{
  int c = a + b;
  return c;
}
int main()
{
  int& ret = Add(1, 2);//err, 7
  Add(3, 4);
  cout << "Add(1, 2) is :"<< ret <<endl;
  return 0;
}

💨在上面我们在 VS 下运行,可以得出编译器并没有清理栈帧,那么这里进一步验证引用返回的危害

虽然能正常运行,但是它是有问题的

 小结引用做返回值

  1️⃣ 出了 TEST 函数的作用域,ret 变量会销毁,就不能引用返回

  2️⃣ 出了 TEST 函数的作用域,ret 变量不会销毁,就可以引用返回

  3️⃣ 引用返回的价值是减少拷贝

❓ 观察并剖析以下代码 ❔

int main()
{
  int x = 3, y = 5;
  int* p1 = &x;
  int* p2 = &y;
  int*& p3 = p1;
  *p3 = 10;
  p3 = p2;
  return 0;
}

💦 函数参数及返回值 ———— 传值、传引用效率比较

#include <time.h>
#include<iostream>
using namespace std;
struct A { int a[10000]; };
A a;
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
A TestFunc3() { return a; }
A& TestFunc4() { return 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;
}
void TestReturnByRefOrValue()
{
  // 以值作为函数的返回值类型
  size_t begin1 = clock();
  for (size_t i = 0; i < 100000; ++i)
    TestFunc3();
  size_t end1 = clock();
  // 以引用作为函数的返回值类型
  size_t begin2 = clock();
  for (size_t i = 0; i < 100000; ++i)
    TestFunc4();
  size_t end2 = clock();
  // 计算两个函数运算完成之后的时间
  cout << "TestFunc1 time:" << end1 - begin1 << endl;
  cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
  //传值、传引用效率比较
  TestRefAndValue();
  cout << "----------cut----------" << endl;
  //值和引用作为返回值类型的性能比较
  TestReturnByRefOrValue();
  return 0;
}

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

💦 引用和指针的区别

1️⃣ 语法概念 1️⃣

引用就是一个别名,没有独立空间,和其引用实体共用同一块空间

指针变量是开辟一块空间,存储变量的地址

int main()
{
  int a = 10;
  int& ra = a;
  cout<<"&a = "<<&a<<endl;
  cout<<"&ra = "<<&ra<<endl;
  int b = 20;
  int* pb = &b;
  cout<<"&b = "<<&b<<endl;
  cout<<"&pb = "<<&pb<<endl;
  return 0;
}

2️⃣ 底层实现 2️⃣

引用和指针是一样的,因为引用是按照指针方式来实现的

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

这里我们对比一下 VS 下引用和指针的汇编代码可以看出来他俩是同根同源

引用和指针的不同点:

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

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

3 、没有 NULL 引用,但有 NULL 指针

4、在 sizeof 中含义不同:引用结果为引用类型的大小,与类型有关;但指针始终是地址空间所占字节个数 (32 位平台下占 4 个字节,64 位平台下占 8 个字节),与类型无关

5、引用自加即引用的实体增加 1,与类型无关,指针自加即指针向后偏移一个类型的大小,与类型有关

6、有多级指针,但是没有多级引用

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

8、引用比指针使用起来相对更安全,指针容易出现野指针、空指针等非法访问问题

七、内联函数

💦 什么是内联函数

在程序中大量重复的建立函数栈帧 (如 swap 等函数) 会造成很大的性能开销,当然 C 语言可以用宏来代替函数,使之不会开辟栈帧,但是宏优点多,但也有不少的劣势,这时内联函数就可以针对这种场景解决问题 (内联函数对标宏函数)。

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

#include<iostream>
using namespace std;
//Add就会在调用的地方展开
inline int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int ret = Add(10, 20);
  cout << ret << endl;
  return 0;
}

❓ 验证 ❔

1、在 release 模式下,查看编译器生成的汇编是否存在 call Add

2、在 debug 模式下,需要对编译器设置,否则不会展开 (因为 debug 下,编译器默认不会对代码进行优化,以下是 VS2017 的设置方式)

💦 内联函数的特性

1️⃣ inline 是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。

2️⃣ inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为 inline 的函数体内有循环/递归等等,编译器优化时会忽略掉内联。

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

// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
  cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
  f(10);
  return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用

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

注意 C++11 一般要标准之后的编译器才支持的比较好 (最少 2013)

💦 auto简介

在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可以思考下为啥?

C++11 中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。

int main()
{
  int a = 3;
  char b = 'A';
  //通过右边的赋值对象,自动推导变量类型
  auto c = a;
  auto d = b;
  //typeid可以去看变量的实际类型
  cout << typeid(c).name() << endl;
  cout << typeid(d).name() << endl;
  return 0;
}

⚠ 注意

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

❓ auto的价值 ❔

auto 在前期并不能很好的体现它的价值,在后面学了 STL 时就能体现了,这里我们可以先看看。

map<string, string> dict;
//map<string, string> :: iterator it = dict.begin();
auto it = dict.begin;//同上

从上就可以看出 auto 的价值就是简化代码

  💨优点:auto 可以自动推导类型简化代码

  💨缺点:一定程度上牺牲了代码的可读性

💦 auto的使用细则

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

  用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别;但用 auto 声明引用类型时则必须加 &

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

2️⃣ 在同一行定义多个变量 2️⃣

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

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

💦 auto不能推导的场景

1️⃣ auto 不能作为函数的参数 1️⃣

//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2️⃣ auto 不能直接用来声明数组 2️⃣

void TestAuto()
{
  int a[] = {1,2,3};
  auto b[] = {4,5,6};
}

3️⃣ 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法 3️⃣

❗ C++98 auto 和 C++11 auto❕

  早期 C++98 标准中就存在了 auto 关键字,那时的 auto 用于声明变量为自动变量,拥有自动的生命周期;但是这个作用是多余的,因为变量默认拥有自动的生命周期

  在 C++11 中,已经删除了这种用法,取而代之的用处是自动推断变量的类型

4️⃣ auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用 4️⃣

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

💦 范围for的语法

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

void TestFor()
{
  int array[] = { 1, 2, 3, 4, 5 };
  for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
    array[i] *= 2;
  for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
    cout << *p << endl;
}

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

void TestFor()
{
  int array[] = { 1, 2, 3, 4, 5 };
  //自动依次取数组中的值给e并自动判断结束
  for(auto e : array)
    cout << e << " ";
  //这里要对数组里的内容进行改变,就要使用&,因为这里的e只是一份拷贝
  for(auto& e : array)
    e *= 2;
  return 0;
}

⚠ 注意:

范围 for 与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。这个东西后面到了容器会详细介绍

💦 范围for的使用条件

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

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

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

void TestFor(int array[])
{
  //注意这里的array已经不是数组了,已经退化为指针了
  for(auto& e : array)
    cout<< e <<endl;
}

2️⃣ 迭代的对象要实现++和==的操作 2️⃣

  关于迭代器这个问题,以后会讲,在这篇文章只做为了解

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

💦 C++98中的指针空值

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

void TestPtr()
{
  //C++98
  int* p1 = NULL;
  int* p2 = 0;  
  //C++11
  int* p3 = nullptr;
  // ……
}

⚠ 注意:

  指针本质是内存按字节为单位的编号,空指针并不是不存在的指针,而是第一个字节的编号,一般我们不使用这个空间存有效数据。空指针一般用来初始化,表示指针没有指向一块有效数据的空间。

❓ 为啥C++11后推荐用nullptr来初始化空指针 ❔

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

场景:这里本意上是想让 0 匹配 int、NULL 匹配 int*

void f(int)
{
  cout<<"f(int)"<<endl;
}
void f(int*)
{
  cout<<"f(int*)"<<endl;
}
int main()
{
  f(0);
  f(NULL);
  //f((int*)NULL);
  //f(nullptr);//使用C++11中的nullptr更准确
  return 0;
}

由于在 C++98 中 NULL 被定义成 0,因此与程序的初衷相悖

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

⚠ 注意:

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

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

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


相关文章
|
1月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
23 0
|
1月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
24 0
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1