C++:与C语言相比的特点

命名空间

先看到一段C语言的代码:

#include 
#include 

int rand = 1;

int main()
{
    printf("%d", rand);
    return 0;
}

这段代码看似没有问题,但是运行后,编译器会报出“rand重定义”的问题。
这是因为我们引入了头文件stdlib.h,而其内部有rand函数,用户的变量名与头文件冲突了。
这该这么解决?
在C语言中,好像没有什么很好的办法,让不同头文件中的同名变量共存,只能让其中一者改变自己的变量名。

C++设计者认为这个特性不利于项目合作,当我们对多个员工编写的C语言代码进行合并时,就有可能出现这个问题,此时只能让其中一者修改代码。
C++为此设计了一种新的域:命名空间域

命名空间域

在不同的域中,是可以存在同名变量的,而C语言只存在局部域与全局域两种域。C++的命名空间域则是一种可以根据用户需要自己定义的域。

  1. 现在当前作用域查找
  2. 如果当前作用域查找不到,就向上级作用域查找
  3. 直到查找到全局作用域,如果此时还没有,编译器报错

 命名空间域有以下特性:

  1. 当两个命名空间域重名,两个域内部的代码会合并
  2. 作用域可以嵌套
  3. 变量,结构体,函数等等都可以写入这个域中
域作用限定符

::是C++中的域作用限定符,将其放在变量前,可以改变此变量的查找规则,使之直接到指定域中查找。

此外:域作用限定符左侧没有值时,默认到全局变量查找。
这一点很重要,因为在基本的查找规则中,是先查找局部作用域,再查找全局作用域的。而当::左侧没有值时,会直接跳过局部变量,在全局中查找。 

比如以下代码:

int a = 3;

int main()
{
	int a = 4;
	printf("%d", ::a);
	return 0;
}

上述代码的输出结果是3。
虽然在局部中有一个a = 4,但是::a会直接跳过局部,直接去全局查找,所以最后输出了3.

访问嵌套的命名空间域:
想要访问嵌套的命名空间域,只需要依据从外层->内层的顺序,利用::将每个名称分隔开,就可以访问了,如下:

namespace A
{
	namespace B
	{
		namespace C
		{
			int a = 2;
		}
	}
}

int main()
{
	printf("%d", A::B::C::a);
	return 0;
}

我们可以按照如下方式解决不同文件变量可能存在冲突的问题:每个.cpp文件最外层,用一个命名空间域包含起来,后续引入文件时,每个人编写的文件独自享有一个域,就不会发生冲突问题了。

user1.cpp

namespace user1
{
	int a = 0;
	int b = 1;

	int Add(int x, int y)
	{
		return x + y;
	}
}

user2.cpp: 

namespace user2
{
	int a = 1;
	int b = 0;

	float Add(float x, float y)
	{
		return x + y;
	}
}

 每一份.cpp文件都用一个命名空间域包在最外层,需要使用谁的代码时,就到哪一个空间域中查找。

展开命名空间域

所谓展开命名空间域,就是对某个空间域进行展开,将其内部的变量放到全局中。也就是说,一个空间域的内容,经过展开后就会变成全局变量,而变量查找规则中,最后一层就是在全局中查找,所以可以不使用::就访问到想要的变量。

using namespace (名称);

但是有时候我们并不是需要一个命名空间域中的所有内容,如果将整个空间域展开有些没必要。
此时我们可以使用部分展开

using (名称)::(变量名) 

缺省参数

全缺省参数

缺省参数是值可以为函数的参数设置初始值,如果调用时没有传入参数,则此参数以初始值调用函数。

int Add(int x = 5, int y = 10)
{
	return x + y;
}

int main()
{
	Add(1, 2);
	Add(1);
	Add();

	return 0;
}

上述代码中,我们定义了一个函数Add,其带有两个参数x和y,其中为x设置初始值x = 5,给y设置初始值y = 10。

第一次调用Add(1, 2);为xy都传了参数,此时完成的是1 + 2。
第二次调用Add(1);只为x传入了参数,此时y以初始值调用此函数,完成的是1 + 10。
第三次调用Add();没有传入参数,此时x和y都以初始值调用此函数,完成的是5 +10。

这种参数缺省叫做全缺省参数,即所有的参数都赋予了初始值,哪怕一个参数都不传,也可以调用函数。
注意:传入参数必须从左往右传入,不能有空缺

半缺省参数

半缺省参数是指,缺省参数时,有一些值不赋予初始值,必须传入值

要注意:半缺省参数中不赋予初始值的参数,必须从左往右连续,不可以间断地缺省。

int Add(int x = 5, int y, int z = 20)
{
	return x + y + z;
}

这种代码就是错误的!!!

最后还有一个注意点:不能在声明和定义时同时缺省参数

当函数的声明和定义中都出现了缺省参数,如果声明和定义缺省不一样,此时编译器不知道是听谁的,因此,缺省参数不能同时存在与声明和定义中。

看到一个示例:
test.h文件中:

void func(int a = 10);

  test.cpp文件中:

void func(int a = 10)
{
	cout << a * 5 << endl;
}

那么应该在什么地方进行缺省呢?

答案是在声明

假设只存在于定义中:函数的声明和定义是在链接阶段才会合在一起,在编译阶段,.h 和 .cpp是在各走各的。在编译阶段,在头文件.h 中,func函数的参数是(int a),编译器并不知道我们在定义中设置的参数的缺省值,因此会发生编译报错。

假设只存在于声明中:函数编译调用函数时主要寻找函数声明,找到函数声明即找到函数地址,这样可以在链接阶段通过函数地址找到函数定义运行,所以在声明时缺省可以让编译器识别传参个数正确与否,从而编译不会报错。

函数重载

函数重载是指C++允许在同一作用域中声明的同名函数,但是其必须遵守一项规则:保证同名函数的形参列表不同。

形参列表不同就是要求满足以下三者之一:

  1. 函数的参数个数不同
  2. 函数的参数类型不同
  3. 函数的参数类型的顺序不同

引用

基本语法

C++的引用是一种特殊的变量类型,用于给已经存在的变量起一个别名。通过引用,我们可以通过一个已存在的变量名来访问和操作另一个变量的值。

引用可以被看作是一个已存在变量的别名,引用和被引用的变量始终指向同一块内存空间,对引用的操作实际上就是对被引用变量的操作。

引用的语法如下:

type& 别名 = 变量名;

 其中,type是被引用变量的类型。

需要注意的是,引用不同于指针,它不能指向空值或者没有初始化的变量。因此,在定义引用时必须保证所引用的变量已经存在,并且在定义引用时必须进行初始化

也就是说下面的语句是非法的:

int& a;

引用其实不单单只是代替指针这么简单,其还可以作为返回值,参数等。

按引用传递

C++中的按引用传递是一种参数传递方式,它允许函数通过引用来操作调用者提供的实参。

按引用传递是将实参的引用传递给形参。

按引用传递的语法是在函数的参数前加上&符号。例如,以下的函数原型中使用了按引用传递:

void Function(int& x);

        

按引用传递有以下几个作用:

  1. 通过引用传递参数可以避免对大型对象的复制。当传递一个大型对象时,按值传递会进行一次复制操作,而按引用传递只需要传递对象的引用而不需进行复制,从而提高了程序的效率。
  2. 通过引用传递参数可以实现函数对实参的修改。在函数内部,通过引用可以直接操作实参,对实参的修改会在函数外部产生影响。而按值传递只能修改函数内部的形参副本,对实参没有影响。

按引用传递是一种高效且灵活的参数传递方式,可以减少内存的复制操作,实现对实参的修改。在C++中,通过引用传递可以提高程序的效率和可读性。

返回引用

在C++中,返回引用是指从函数中返回一个引用类型的值。返回引用的主要目的是允许函数返回一个对于某个变量的引用,从而允许在函数外部对该变量进行修改。

返回引用的主要用途有以下几个:

  1. 允许函数直接修改函数外部的变量。
  2. 允许在函数调用中连续进行操作,类似于链式操作。
  3. 优化性能,避免创建临时对象。

下面通过案例来分别说明这几个功能: 

  1. 允许函数直接修改函数外部的变量:
    int& increment(int& num) {
      num++;
      return num;
    }
    
    int main() {
      int num = 5;
      increment(num) = 10;
      cout << num << endl;  // 输出为 10
      return 0;
    }
    

    在上面的例子中,increment函数返回了对num的引用。在main函数中,我们可以直接对increment(num)进行赋值操作,相当于对num进行了修改。

  2. 允许在函数调用中连续进行操作:
    int& add(int& num, int value) {
      num += value;
      return num;
    }
    
    int main() {
      int num = 5;
      add(add(num, 3), 2);
      cout << num << endl;  // 输出为 10
      return 0;
    }
    

    在上面的例子中,add函数返回了对num的引用。我们可以连续调用add函数,每次都对num进行修改。

优化性能,避免创建临时对象:

string& concatenate(string& str1, const string& str2) {
  str1 += str2;
  return str1;
}

int main() {
  string str1 = "Hello";
  string str2 = " World";
  concatenate(str1, str2) += "!";
  cout << str1 << endl;  // 输出为 "Hello World!"
  return 0;
}

在上面的例子中,concatenate函数返回了对str1的引用。通过返回引用,我们可以直接对str1进行修改,避免了创建临时对象。在调用concatenate函数的时候,我们可以将返回的引用与另一个字符串连接操作进行连续调用。

需要注意的是,返回引用时,被返回的变量应该仍然存在(即出了作用域变量的生命周期还没结束),否则返回的引用就会变成悬空引用,可能导致不可预期的行为。此外,如果返回引用指向了一个局部变量,函数返回后该变量将被销毁,返回的引用将变得无效。因此,返回引用时需要确保引用的有效性。

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

内联函数

在讲解内联函数前,我们前看看C语言中的宏的缺点。
C语言宏的缺点有以下几个:

1.没有类型检查: 宏是在预处理阶段进行替换,没有类型检查的机制。因此,使用宏时要特别小心,否则可能会出现类型不匹配的错误。
2.可读性差: 宏通常会展开为较长的代码,可能会使代码变得难以阅读和理解。特别是在宏内部使用复杂的表达式或多行代码时,会使代码的可读性大大降低。
3.可能引起副作用: 宏通常会直接对参数进行替换,可能会导致意外的副作用。例如,一个宏可能会多次计算参数的值,如果参数是一个函数调用或者是一个带有副作用的表达式,那么可能会引发错误。
4.可能导致重复的代码: 使用宏可能导致代码中出现大量的重复代码。当多个地方使用相同的宏时,如果需要修改宏的实现方式,就需要修改所有使用该宏的地方,增加了代码维护的复杂性。
5.调试困难: 宏在展开后的代码中看不到宏本身的定义,因此在调试时很难跟踪和查找问题。由于宏在编译阶段被替换,调试器无法直接定位到宏的定义位置,这给调试带来了一定的困难。

C++认为宏是一个不太好的特性,于是在C++中推荐使用enum枚举和const替换掉宏常量。用内联函inline替换掉宏函数。 

被inline修饰的函数叫做内联函数,在编译时C++编译器会在调用内联函数的地方将内联函数展开,不额外创建栈帧来执行函数,提高程序的效率。

本质上是函数指令在执行处直接展开。所以如果宏函数非常长,那么对其展开时会导致代码重复性非常高,这已经违背了函数设计的初衷:代码复用。
所以内联函数有另外一个特性:当函数体内部代码长度超过一定值时,其会转化为普通函数,不会直接展开,而是创建栈帧,防止代码冗余。

auto

在C++中,auto关键字可以用来自动推断变量的类型,它在编译时会根据初始化表达式的类型来确定变量的类型。

auto与指针和引用结合

auto也可以自动推断指针的类型,比如这样:

int x = 10;
auto y = &x;

此时y的类型自动判别为int*
那么我们可不可以为auto加上*来识别指针?

int x = 10;

auto* a1 = x;
auto* a2 = &x;
auto a3 = &x;

在auto* a1 = x;中,x的类型是int,那么auto本应将其值判别为int,但是由于auto*被*限制了,此时auto必须得到一个指针,所以编译器会报错;而auto* a2 = &x;得到的就是指针,此时代码不会报错,可以正常识别为int*。

在本质上auto* a2 = &x;和auto a3 = &x;的结果是没有区别的,只是auto*要求得到的必须是一个指针类型,而auto不限制其类型。
同理的auto&会要求必须是一个引用类型,否则会报错。

auto也有许多限制,要注意以下问题:

  1. auto不能作为函数的参数

  2. auto不能用于声明数组

  3. 在同一行定义多个变量时,如果将auto作为其类型,必须一整行都是同一个类型的变量。

范围for循环

范围for循环是C++11引入的一种新的循环结构,它可以方便地遍历数组或者其他具有迭代器的对象。

范围for循环的语法如下:

for (auto element : collection) {
   // 执行语句
}

其中,element 是一个临时变量,用来存储集合中的每个元素的副本,collection 是一个可迭代的对象,可以是数组或者其他具有迭代器的对象。
其中auto也可以换为intfloat等类型,只是结合auto会更好用。

int main() {
   int numbers[] = {1, 2, 3, 4, 5};

   for (auto element : numbers) {
      cout << element << " ";
   }

   return 0;
}

输出结果为:1 2 3 4 5

在上面的例子中,我们定义了一个整型数组 numbers,范围for循环遍历了整个数组,每次迭代将数组中的一个元素赋值给临时变量 element,然后我们将该元素输出到控制台。

如果你希望修改这个数组内部的值,可以在auto后加上&,将其变为一个引用。

   for (auto& element : numbers) {
      element *= 2;
   }

nullptr

在C++11标准中,引入了nullptr关键字来表示空指针。C++推荐使用nullptr而不是使用传统的NULL宏定义。

NULL在传统的C++中只是一个宏定义为0,会被隐式转换为整型,这可能导致一些类型安全性问题。nullptr不会被隐式转换为其他类型,只能赋值给指针类型,从而避免了潜在的类型错误。

其次是代码清晰度,nullptr相比于NULL更加直观明了,能够更好地表示空指针的含义即null + ptr,null表示空ptr表示指针。这样可以提高代码的可读性。

所以在C++中,定义一个空指针最好用nullptr。

你可能感兴趣的:(C++,c++,c语言,算法)