C++:入门学习C++,它在C的基础上做了哪些修改?

简介: C++:入门学习C++,它在C的基础上做了哪些修改?

命名空间

首先看这样的代码:

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

上述代码可以编译通过吗?很明显是不可以的,原因在于头文件stdlib中包含了rand函数,因此在定义变量时就不可以再继续使用rand作为你的变量了

而在未来写工程项目中,这样的情况会遇见很多,在包含某个头文件后,代码中的许多变量就不可以再使用了,这就体现出了C语言的一部分局限性

那么C++在C语言的基础上很好的改善了这个问题,C++引入了命名空间的概念,把变量命名在某个空间内,这样就能很好的解决这个问题

命名空间的定义很自由,可以定义变量,定义函数,定义结构体,甚至可以嵌套定义

namespace zbh
{
  //定义变量
  int test = 0;
  //定义函数
  int Add(int x, int y)
  {
    return x + y;
  }
  //定义结构体
  struct MyStruct
  {
    int a;
    int b;
  };
  //命名空间可以嵌套
  namespace free
  {
    int print1()
    {
      return 1;
    }
  }
}

命名空间是如何使用的?C++如何保证命名空间的独立性?

  1. 使用变量时单独说明
  2. 前面定义使用命名空间中的某个函数或变量等
  3. 直接展开
#include <stdio.h>
namespace zbh
{
  //定义变量
  int test = 0;
  //定义函数
  int Add(int x, int y)
  {
    return x + y;
  }
  //定义结构体
  struct MyStruct
  {
    int a;
    int b;
  };
  //命名空间可以嵌套
  namespace free
  {
    int print1()
    {
      return 1;
    }
  }
}
using zbh::Add;
int main()
{
  printf("%d\n", zbh::test);
  printf("%d\n", Add(1,2));
  printf("%d", zbh::free::print1());
}

上面对命名空间的定义也可以省略,直接在main函数前加上

using namespace zbh;

即可直接在后面的函数中使用,通过这样的方式即实现了函数命名空间的独立化

缺省参数

C++中对于函数参数定义了缺省参数,可以理解为,如果我对函数参数中的成员赋给了它一个初值,那么在后续调用的过程中,如果我并未给函数传参,那么函数就会使用默认的参数

具体样例如下所示

#include <iostream>
using namespace std;
void f(int a = 10, int b = 20, int c = 30)
{
  cout << a << " " << b << " " << c << endl;
}
int main()
{
  f();
  f(1);
  f(1, 2);
  f(1, 2, 3);
  return 0;
}

运行结果如下:

在实际中这样的操作有什么作用??

在定义顺序表中,我们使用的是动态开辟的顺序表,那么在初始化阶段我们是不是可以利用缺省参数优化一些步骤?

首先看C语言实现过程中的方法

#include <stdio.h>
#include <stdlib.h>
typedef int SLDataType;
typedef struct Seqlist
{
  SLDataType* a;
  int size;
  int capacity;
};
void SeqlistInit(Seqlist* s)
{
  s->a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
  s->size = 0;
  s->capacity = 4;
}

这样的实现实际上把容量写死了,不管要开辟多大的顺序表我们都是先开辟容量为4的顺序表再后续进行扩容,而扩容用的realloc是有消耗的

但假设如果我们使用缺省参数进行实现这个函数,可以优化很多

#include <iostream>
using namespace std;
typedef int SLDataType;
typedef struct Seqlist
{
  SLDataType* a;
  int size;
  int capacity;
}Seqlist;
void SeqlistInit(Seqlist* s,int capacity=4)
{
  s->a = (SLDataType*)malloc(sizeof(SLDataType) * capacity);
  s->size = 0;
  s->capacity = capacity;
}
int main()
{
  Seqlist sq,sl;
  SeqlistInit(&sl);
  SeqlistInit(&sq, 100);
  return 0;
}

调用监视观察可以看到

利用缺省参数,我们确实实实在在实现了自由确定自己想要的容量

函数重载

C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型

不同的问题。

下面展示了什么是函数重载

#include <iostream>
using namespace std;
void func(int a, double b)
{
  cout << "void func(int a, double b)" << endl;
}
void func(double b, int a)
{
  cout << "void func(double b, int a)" << endl;
}
int main()
{
  func(1, 1.1);
  func(2.2, 2);
  return 0;
}

当同名函数中参数个数或参数类型不同时,构成了函数重载,编译器会自动识别传参的个数和类型对应不同的函数重载类型

那为什么C++支持,但是C语言不支持呢?

这里需要讲到一个程序运行起来需要经历的过程

预处理,编译,汇编,链接

假设现在定义了一个Add函数,这个Add函数是在a.cpp文件中定义的,而我要在b.cpp中使用这个函数,当使用后运行程序时,编译器要在b.cpp中使用该函数,但是却找不到这个函数的地址,因此编译器会去b.cpp的符号表中去寻找

Add的地址,然后把这两个链接在一起

在链接的过程中,C和C++就产生了不一样的地方,每个编译器都有自己专属的函数名修饰的规则

下面展示的是C语言编译器的展示结果:

从中可以看出,在编译完成后,函数名字的修饰依旧是函数的名字,没有实质性的变化

下面再看C++编译器中的样例

对比C语言的编译器可以很清楚的看到,C++的编译器在编译的过程中把函数名进行了一定程度的修饰,在Linux的编译器下是把函数的参数类型和参数个数也加到了函数名字当中

因此,从中就知道了,为什么C语言不支持函数重载,而C++支持了,就是因为C语言编译器对函数名就是函数名,而C++的编译器对函数名做了参数的引入进行修饰,因此重载后的函数有不同的函数名

从中也就能很轻松的理解,为什么函数参数的类型和个数一样,只有返回值不一样的时候,无法构成函数重载,就是因为编译器对于两个同名的函数不知道该调用哪一个

引用

C++在C的基础上做出的另一大调整就是引用

简单来说,引用就是给变量起了一个别名,变量本身和引用一起控制变量所在的区域

有下面的代码:

void test2()
{
  int a = 10;
  int& b = a;
  cout << &a << endl;
  cout << &b << endl;
}

那么这个程序的运行结果是多少呢

从中可以看出,引用并不是单独再开辟一块空间用来管理所指向的对象,而是直接和原来的变量一起控制某块区域内的内容

引用的一些特性

  1. 引用在定义的时候必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦确认了一个实体,就不能引用其他内容了

引用作为函数返回值

引用作为函数返回值是一件危险的事,但如果使用正确是有高回报的

我们以下面的操作为例

int& Count()
{
  int n = 1;
  n++;
  return n;
}
void test2()
{
  int a = 10;
}
int main()
{
  int& ret = Count();
  cout << ret << endl;
  test2();
  cout << ret << endl;
  return 0;
}

输出结果为2和10,这是为什么?

原因上升到了函数栈帧的问题,首先画出函数栈帧

下面画出的是main函数执行前两行时候的操作,后续还未画出

这里定义了&ret,它接收了来自Count函数中的返回值int& n,实际上,这里的ret已经具有了管理n那块区域的能力,而我们又知道,函数在结束后栈帧会被销毁,这里的销毁只是失去了对内存中这片区域的管理权,内存中这块区域本身还是存在的

因此,当下面执行test2函数时,又会开辟一块新的栈帧,而这块栈帧所在的位置和Count函数的栈帧是有很大重合区域的

因此,这里的ret已经具备了管理不属于它的空间的能力,它可以随时访问一块已经不属于它的区域,因此这里原本是n的空间,现在已经变成了a,但是它依旧可以访问,因此访问出的结果就是10了

因此,这里有需要注意的地方:

==如果函数返回时,出了函数的作用域,如果返回对象还在(没有还给系统),那么这里就可以使用引用返回,如果是像这样的临时变量或者局部变量,就必须使用传值返回

那么说回来,既然引用作为返回值或者参数如此危险,那它有什么使用的必要?

引用的收益

以函数传参为例,如果不用传引用会在函数传参的时候构建一个形参,如果传递的是对象,那么会执行很多默认的成员函数,这也是性能上的损耗,而如果传递的是引用,直接把已经创建好的对象引用到这里,略去了形参创建再销毁这个过程

传引用传参的优势

  1. 提高效率
  2. 输出型参数(形参的改变可以影响实参,类似于指针,但比指针简单一些)

继续谈作为引用返回的优势,作为引用返回也是诸多好处,当然是使用正确的前提;有这样的原则,什么时候可以使用传引用返回?结论是当返回的内容出了函数作用域不被销毁就可以,这是有相当高收益的,在类内的很多成员函数中,返回的通常是*this,而this并不会被销毁,这时用传引用的返回值就很值得

传引用返回的优势

  1. 提高效率
  2. 可以修改返回对象

综上所述, 传值传参还是传值返回值都会创建对象,而使用引用就避免了无效的,多余的创建带来的性能损耗

引用和指针的区别?

语法概念来讲引用是别名,没有自己的空间,它存在的本身就是和实体公用一块空间

但是

底层实现上引用是有自己的空间的,引用本身就是用指针的方式实现的

引用和指针的对比

引用和指针的不同点:

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

内联函数

首先,什么是内联函数?

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调

用建立栈帧的开销,内联函数提升程序运行的效率。

我们都知道,当main函数执行到某个函数时,会在内存中建立该函数的栈帧,再call(调用),而建立函数栈帧是有成本的,如果频繁的建立一些可能只有很少语句的函数会浪费程序运行的效率,因此内联函数的产生就解决了这个问题

内联函数工作的原理,就是在编译阶段,直接把函数体替换为函数调用,从汇编中可以看出,正常函数是有函数调用和函数栈帧的创建的,而内联函数直接把函数体全部都放到汇编

现在我们有这样的代码:

int add(int a, int b)
{
  return a + b;
}
int main()
{
  int a = 1;
  int b = 2;
  int c = add(a, b);
} 

通过汇编来观察

当执行到add函数时,确确实实是调用了add函数,那么假如把add函数设为内联函数

inline int add(int a, int b)
{
  return a + b;
}
int main()
{
  int a = 1;
  int b = 2;
  int c = add(a, b);
}

再来看它的汇编代码,就会发现不再是函数调用,而是直接把函数体展开

内联函数的特性

这么来看,内联函数确实是很有用,但内联函数有它不可忽视的弊端

内联函数是一种用空间换时间的方法,不可否认,在这里略去了函数建立栈帧所需要的时间消耗,但是在编译阶段,内联函数会把函数调用全部替换为函数体,这会使得目标文件变大

因此,内联函数只是一个建议,只是建议编译器可以把这个函数当成内联函数来处理,但是具体到底有没有把它当成内联函数来处理还要看编译器本身,一般来说,会把函数规模比较小,不是递归函数,调用很频繁的函数设置为内联函数

同时,内联函数不应该声明和定义分离,内联函数被展开后,函数地址也就不复存在了,链接过程中就无法找到链接

内联函数和宏的关系

在C语言中,引入了宏,宏看似是一个很好的功能,但其中也有很多弊端

宏的优缺点?

优点:

  1. 增强代码的复用性。
  2. 提高性能。

缺点:

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

因此,内联函数的出现也算是弥补了宏定义函数带来的诸多不便

指针空值问题

在良好的编程习惯中,定义一个变量要给它一定的初始值,因此在C语言中,我们定义一个指针时,对它的初始化常常是NULL

NULL实际上是一个宏

#define NULL  0

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

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,因此与程序的初衷相悖。

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

因此,在C++的新标准中就引入了关于空指针:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
相关文章
|
1天前
|
编译器 C语言 C++
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
【C++入门学习指南】:函数重载提升代码清晰度与灵活性
9 0
|
1天前
|
安全 编译器 程序员
【C++入门】内联函数、auto与基于范围的for循环
【C++入门】内联函数、auto与基于范围的for循环
|
1天前
|
存储 安全 编译器
【C++入门】缺省参数、函数重载与引用(下)
【C++入门】缺省参数、函数重载与引用
|
1天前
|
编译器 C语言 C++
【C++入门】缺省参数、函数重载与引用(上)
【C++入门】缺省参数、函数重载与引用
|
1天前
|
C语言 C++
【C++入门】关键字、命名空间以及输入输出
【C++入门】关键字、命名空间以及输入输出
|
1天前
|
人工智能 分布式计算 Java
【C++入门】初识C++
【C++入门】初识C++
|
1天前
|
C++ Python
C++教学——从入门到精通 10.循环
学习编程建议先Python后C++,以避免C++思维影响。课程涵盖for、while和do while循环。for循环示例:`for(int i=0;i&lt;n;i++)`,用于计算114514天后的金币总数(1145140个)。死循环通过`for(int i=0;;i++)`实现,用`break`退出。while循环格式`while(条件)`,同样可解决金币问题。do while循环特点是先执行后判断,结构为`do{...}while(条件)`。
8 2
|
2天前
|
C++
C++入门项目——通讯管理系统
C++入门项目——通讯管理系统
|
2天前
|
存储 编译器 C++
C++基础入门(超详细)
C++基础入门(超详细)
|
6天前
|
设计模式 存储 算法
C++从入门到精通:3.7阅读和理解开源项目——在实际项目中应用C++编程技巧和规范
C++从入门到精通:3.7阅读和理解开源项目——在实际项目中应用C++编程技巧和规范