前言
这篇博客主要介绍C++是如何弥补C语言在某些地方的不足。
接下来一起看看吧。
一、命名空间
1.0 C语言中的问题
#include <stdio.h>
#include<stdlib.h>
int rand = 10;
int main()
{
//rand在stdlib.h是一个函数,上面又对它定义了一次。
printf("%d ", rand);
return 0;
}
问题:
- 程序员定义的与C语言标准库定义的变量名、函数名等冲突
- 程序员之间定义的变量名、函数名等冲突。
1.1 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
举个例子:
//example是命名空间的名字
namespace example
{
int rand=5;
int printf=10;
//也可以定义函数
int Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
}
1.2 命名空间的特点
1.2.1 可以嵌套定义
namespace A
{
int a = 1;
int b = 2;
namespace B
{
int a = 2;
int b = 1;
}
}
1.2.2 在同一个工程中的相同名称的命名空间会合并
//test.h
namespace A
{
int i = 10;
double d = 5.20;
}
//Test.cpp中
#include "test.h"
namespace A
{
int a = 1;
int b = 2;
namespace B
{
int a = 2;
int b = 1;
}
}
int main()
{
printf("d\n", A::i);//i在Test.cpp中没有定义,但是能读取说明test.h和Test.cpp中同名的命名空间已经合并了。
printf("%d\n", A::a); // "::"作用域限定符
printf("%d\n", A::B::a);
return 0;
}
1.3 命名空间的使用
1.3.1 方法一:命名空间+域作用限定符
namespace A
{
int a = 1;
int b = 2;
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
printf("%d\n", A::a); // "::"作用域限定符
printf("%d\n", A::Add(5 ,56));
return 0;
}
1.3.2 方法二:使用using namespace 命名空间名称 引入
namespace A
{
int a = 1;
int b = 2;
int Add(int x, int y)
{
return x + y;
}
}
//展开全部命名空间:using namespace 命名空间的名称
using namespace A;
int main()
{
printf("%d\n", a); // "::"作用域限定符
printf("%d\n", Add(5 ,56));
return 0;
}
1.3.2 方法三:使用using将命名空间中某个成员引入
namespace A
{
int a = 1;
int b = 2;
int Add(int x, int y)
{
return x + y;
}
}
using A::Add;
int main()
{
//由于a没有展开,所以a不能被读取
printf("%d\n", a); // "::"作用域限定符
printf("%d\n", Add(5 ,56));
return 0;
}
二、缺省参数
2.1 概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
例:
#include <iostream>
using namespace std;
void Fun1(int x = 10)
{
cout << x << endl;
}
int main()
{
Fun1();//没有给实参
Fun1(5);
return 0;
}
运行结果:
2.2 缺省参数的分类
2.2.1 全缺省
//函数的全部参数都是缺省参数
void Fun(int x = 10, int y = 15, int z = 20)
{
cout << x << " " << y << " " << z << endl;
}
int main()
{
//Fun(8, ,10) 这样填参数是会报错的。
Fun();
Fun(55, 11, 22);
return 0;
}
2.2.2 半缺省
//函数的部分参数是缺省参数(从右往左依次缺省)
void Fun(int a, int b, int c = 5)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Fun(5, 20);
Fun(5, 20, 10);
return 0;
}
2.3 特点
- 半缺省一定要从右往左依次为缺省参数,不能间隔。
- 缺省参数的声明和定义不能都为缺省参数,一般是声明给缺省参数就可以了。
- 缺省值一般是常量和全局变量。
2.4 应用场景
当我们要开一个动态数组时,但不知道它要开辟多大的空间时。学了缺省参数后我们就可以这样写了。
//确定要多大就写实参,不确定就不写实参。
void ListInit(int* a, int capicity = 4)
{
int* a = (int*)malloc(sizeof(int) * capicity);
if (a == NULL)
{
//...
}
//...
}
三、 函数重载
3.1 概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
3.2 类型
3.2.1 参数个数不同
//1.参数个数不同
int Add(int x, int y)
{
return x + y;
}
int Add(int x, int y, int z)
{
return x + y + z;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1, 2, 3) << endl;
return 0;
}
3.2.2 参数类型不同
//2.参数类型不同
int Add(int x, int y)
{
return x + y;
}
float Add(float x, float y)
{
return x + y;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.2f, 2.1f) << endl;//数值后加f说明它的类型是float。
return 0;
}
3.2.3 参数类型的顺序不同
//3.参数类型的顺序不同
void Fun(int i, char c)
{
cout << "Fun i c" << endl;
}
void Fun(char c, int i)
{
cout << "Fun c i " << endl;
}
int main()
{
Fun(1, 'c');
Fun('c', 1);
return 0;
}
3.3 为什么C语言不支持函数重载而C++支持
主要就是它们两的函数名修饰规则不同决定的。
因为C语言同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
四、引用
4.1 概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
例:
//类型& 引用变量名=引用实体
int main()
{
int a = 10;
int& b = a; //b是a的应用,b的改变会影响a。a也会影响b。
cout << b << endl;
b++;
cout << a << endl;
cout << b << endl;
return 0;
}
要注意的是:引用类型要与引用实体的类型相同。
4.2 引用特性
- 引用必须初始化。
- 一个变量可以有多个引用。
- 引用一旦初始化是不能改变引用实体的。
4.3 使用场景
4.3.1 作参数(输出性参数,提高效率)
//做参数
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 5;
cout << a << " " << b << endl;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
4.3.2 作返回值
//引用作返回值
int& Fun()
{
int a = 1;
a++;
return a;
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
//函数Fun返回了临时变量,临时变量的值可以是随机值。
int& a = Fun();
Add(4, 5);
cout << a << endl;
return 0;
}
特点:
- 它相对的减少了拷贝,也提高了效率。
- 它可以读写返回值,但这也带有一些风险。
4.4 引用小总结
- 基本任何场景下,我们都可以用引用传参。
- 我们要谨慎用引用作返回值。如果对象出来函数作用域被销毁了,就不能使用引用返回对象了。如果对象出来函数作用域没有被销毁,就可以使用引用返回。
- 在语法层面:不会开空间,只是对变量起别名。 在底层汇编指令实现的角度:是类似指针的方式实现的。
4.5 常引用
只有引用时才考虑权限的问题。
例1:
int main()
{
int a = 10;
int& b = a;
//这里权限被放大了,权限只能被平移和缩小
//const int d = 10;
//int& e = d; 所以这是错误的写法
//权限被缩小了
const int& c = a;
b++;
//c++; //c不能加加了,
//这里是这里权限的平移
const int i = 5;
const int& z = i;
return 0;
}
例2:
int main()
{
int a = 10;
int& b = a;
//这里权限被放大了,权限只能被平移和缩小
//const int d = 10;
//int& e = d; 所以这是错误的写法
//权限被缩小了
const int& c = a;
b++;
//c++; c不能加加了,
//这里是这里权限的平移
const int i = 5;
const int& z = i;
return 0;
}
例3:
int funA()
{
static int s_i = 10;
s_i++;
return s_i;
}
int& funB()
{
static int s_i = 10;
s_i++;
return s_i;
}
int main()
{
//传值返回,赋值时会产生一个临时变量辅助赋值。但临时变量有常性。
int& a = funA();//权限的放大,会报错。
int& b = funB();//权限的平移。
const int& c = funB();//权限的缩小。
return 0;
}
总结:
- 当传值返回或发生隐形类型转换时,会产生一个临时变量辅助赋值。但它具有常性(类似被const修饰)。
- 只有在引用时,才会有权限的问题。
- 权限只能被平移和缩小,一定不能被放大。
4.6 引用和指针的区别
- 语法层面:引用是不会开空间的,它对对象起别名。而指针会开空间存放的是对象的地址。
- 引用必须初始化。而指针没有要求。
- 没有多级引用。而指针有多级指针。
- 引用没有NULL。而指针可以是NULL。
- 引用一旦初始化后就不能改对象了。而指针可以改内容。
- 引用比指针更安全。指针有野指针或NULL指针访问的问题。
- 引用加1是对实体加1,而指针+1是向后移动一个指针类型的大小。
- sizeof()求引用的大小是引用类型的大小。而sizeof求指针大小:在32位机器上是4个字节,在64位机器上是8个字节。
五、关键字:auto
5.1 使用场景
使用auto修饰的变量,是具有自动存储器的局部变量。
例:
int main()
{
//auto具有自动推导类型的功能。
//1.能和指针、引用结合使用。
int a = 10;
auto* pa = &a;
auto& aa = a;
auto d = 1 + 1.2;
//打印变量类型
cout << typeid(pa).name() << endl;
cout << typeid(aa).name() << endl;
cout << typeid(d).name() << endl;
auto i = 4, j = 5;
//auto ii = 4, dd = 4.6; 报错//一行不能推导两个类型
return 0;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
5.2 不能推导的场景
- 不能作函数的参数。
- 不能直接用来声明数组。
六、基于范围的for循环
6.1 概念
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
6.2 使用条件
- for循环迭代的范围必须是确定的。
int main()
{
int arr[] = { 2, 4, 6, 8 };
//适用于数组
//依次将数组中数据赋值给e
//自动迭代,自动判断介绍。
for (auto x : arr)
{
cout << x <<" ";
}
//修改数组中的数据
for (auto& e : arr)
{
e *= 2;
}
cout << endl;
for (auto x : arr)
{
cout << x << " ";
}
return 0;
}
七、内联函数
7.1 概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
7.2 特性
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
例2.1:
//函数规模较小,会成为内联函数。
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
//多次调用
for (int i = 0; i < 10000; ++i)
{
cout << Add(i, i + 1) << endl;
}
return 0;
}
例2.2
//inline 只是对编译器的一个建议,函数是否为内联函数取决与编译器,如果函数体过长、没有频繁调用、是递归函数就不会成为内联函数。
inline void Func()
{
cout << "hhhhhhhhhhh" << endl;
cout << "hhhhhhhhhhh" << endl;
cout << "hhhhhhhhhhh" << endl;
cout << "hhhhhhhhhhh" << endl;
cout << "hhhhhhhhhhh" << endl;
cout << "hhhhhhhhhhh" << endl;
}
int main()
{
Func();
return 0;
}
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
例3.1
//test.h中代码
#include <iostream>
#include <assert.h>
using namespace std;
inline void Fun();
//test_2.cpp
#include "test.h"
inline void Fun()
{
cout << "Fun" << endl;
}
//test.cpp
int main()
{
Fun();
return 0;
}
报错原因:
八、指针空值nullptr
在C语言中,NULL本质上是宏定义的。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在一些特定的场景,例如:
void Func(int)
{
cout << "FuncA" << endl;
}
void Func(int* )
{
cout << "FuncB" << endl;
}
int main()
{
Func(0);
//本来是想调用第二个函数,但调用了第一个函数
Func(NULL);
Func((int*)NULL);
return 0;
}
8.1 nullptr关键字
- nullptr是C++11作为新关键字引入的,用nullptr表示指针空值时,不需要包含头文件。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr
例:
int main()
{
int a = 10;
int* pa = nullptr;
pa = &a;
return 0;
}