欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.comhttps://gitee.com/BingbingSuperEffort
目录
前言
从本章开始,冰冰将进入新的章节C++的学习。
1、C++关键字
C++含有63个关键字,其中包含了C语言的关键字。下表为C++关键字汇总。
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | size of | typename | throw |
case | enum | mutable | static |
union
|
wchar_t
|
catch
| explicit | namespace | static_cast | unsigned | default |
char
|
export
|
new
|
struct
|
using
|
friend
|
class
|
extern
|
operator
|
switch
|
virtual
|
register
|
const
|
false
|
private
|
template
|
void
|
true
|
const_cas
|
float
|
protected
|
this
|
volatile
|
while
|
delete
|
goto
|
reinterpret_cast
|
2、命名空间
2.1命名空间定义
命名空间在定义时需要使用关键字:namespace,后面跟上命名空间的名字,然后用一对大括号将命名成员括起来,如下所示。
namespace lb
{
int a=0;
void test()
{
printf("hello world!\n");
}
namespace N1
{
int b=0;
}
}
命名空间中的内容既可以定义变量,也可以定义函数,并且命名空间还可以嵌套使用。注意:在同一个工程中含有多个相同名称的命名空间,编辑器在最后会将其合成同一个命名空间。例如,官方标准库的命名空间名为std,如果我们使用std进行了命名空间的命名,那么会将其与官方的命名空间合并在一起。
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
2.2命名空间的使用
那么命名空间定义完毕我们怎么使用呢?例如下面的代码在使用中会发生什么呢?
namespace lb
{
int a = 0;
int b = 10;
void test()
{
printf("hello world!\n");
}
}
int main()
{
printf("%d\n", a);
return 0;
}
我们会发现,当我们运行函数时,编译器会进行报错,因为它找不到变量a。原因在于变量a虽然被定义了,但是定义在了lb的命名空间内,编译器在寻找时,由于没有指定变量a的具体范围,他会默认去全局寻找,找不到就会报错。
正确使用命名空间的方式有三种:
(1)加命名空间名称和作用域限定符
int main()
{
printf("%d\n", lb::a);
lb::test();
return 0;
}
此种方式使用了域作用限定符"::",该操作符左操作数为命名空间的名称,如果为空白则默认为全局域,右操作数为变量名称,也可以是函数引用。
(2)使用using将命名空间中的成员引入
using lb::b;
int main()
{
printf("%d\n", lb::a);
printf("%d\n", b);
return 0;
}
此种方式为工程中最常用的方式,仅仅展开命名空间中常用的变量或者函数名称,并不会将整个命名空间展开。
(3)使用using namespace 命名空间名称引入
using namespace lb;
int main()
{
printf("%d\n", lb::a);
printf("%d\n", b);
test();
return 0;
}
3、C++的标准输入和输出
在前面的练习代码中,我们虽然使用了C++的一些语法,可是在进行输入输出的时候使用的还是C语言的函数,虽然C++兼容C语言,但是用起来总感觉并不是那么专业。
C++也具有输入输出函数,这里只进行简单介绍,后期会有详细讲解。C++的输入函数为cin,输出函数为cout,在使用时必须包含头文件<iostream>以及标准命名空间std。
//C++
using std::cout;
using std::cin;
using std::endl;
int main()
{
int i = 0;
double d = 23.3l;
cin >> i >> d;
cout << i << ' ' << d << endl;
return 0;
}
//C
int main()
{
scanf("%d %lf", &i, &d);
printf("%d %lf\n", i, d);
return 0;
}
上面代码为C++和C的标准输入与输出函数的使用,我们发现,在C++中并没有格式的规定,而只有变量以及一些操作符。">>","<<"为流插入与流提取操作符,"endl"则相当于"\n"是一个换行符。使用C++进行输入输出会比C语言更加简单,原因在于C++中不需要数据格式控制,函数会自动识别。但是在进行格式化输出时,C++就没有C语言这么方便,好在C++可以兼容,我们完全可以继续使用C语言进行操作。
4、缺省参数
4.1缺省参数的定义
什么是缺省参数,缺省参数是声明或定义函数时为函数的参数指定的一个默认值。在调用该函数时,如果制定了实参,则函数使用指定的实参进行调用,如果未指定,则按照默认值进行调用。
void test1(int a = 0)//缺省值--没有传具体参数时,使用缺省参数
{
cout << a << endl;
}
int main()
{
test1();//打印0
test1(1);//打印1
return 0;
}
4.2缺省参数的分类
缺省参数分为两种,全缺省参数,半缺省参数。
(1)全缺省参数
全缺省参数即为函数参数中的所有参数都进行了默认值的设计,在函数调用过程中,如果一个参数都没有给定,将按照默认值进行打印,如果给定一个参数,则将从左到右进行传参,即test2(1),函数调用过程中,将会打印a的值为1,b的值为默认值。注意:test2(,2)为非法行为。
void test2(int a = 10, int b = 20)//全缺省--参数将从左往右给定
{
cout << a << endl;
cout << b << endl;
}
int main()
{
test2();//a=10,b=20
test2(1);//a=1,b=20
test2(1,2);//a=1,b=2
return 0;
}
(2)半缺省参数
顾名思义,半缺省参数就是只有一部分参数进行了默认值设置,须符合从右到左进行缺省,并且不能有间隔。
void test3(int a, int b = 20, int c = 30)//半缺省--只能从右往左进行连续缺省,不能间隔
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
test3();//非法传递
test3(1);
test3(1,2);
return 0;
}
4.3缺省参数的使用样例
缺省参数有什么用呢?还记得我们在设计栈的初始化函数的时候吗?那时我们在调用初始化函数的时候,会将capacity进行初始空间开辟,初始开辟多少都是我们自己设定的默认值,也就是4个空间,但是如果我知道我使用这个栈需要存储100个数据,那么栈直接给我一次性开辟100个空间将会省略频繁判断是否容量已满而一直开辟空间的操作。这里我们就可以使用缺省函数进行操作。
struct Stack
{
int* _a;
int _n;
int _capacity;
};
void StackInit(struct Stack*ps,int capacity=4)
{
int* tmp = (int*)malloc(sizeof(int) * capacity);
if ( tmp == NULL )
{
perror("malloc");
exit(-1);
}
ps->_a = tmp;
ps->_n = 0;
ps->_capacity = capacity;
}
int main()
{
struct Stack s1;
struct Stack s2;
StackInit(&s1);
StackInit(&s1,100);
return 0;
}
s1在传参的时候并没有进行容量传参,而默认开辟4个空间,s2指定了100个容量的空间,则可以直接开辟100个。
这里还要注意一点,含有缺省参数的函数只在声明中进行缺省参数设定,函数定义中不进行缺省。缺省值必须是常量或者全局变量,并且C语言的编译器并不支持。
5、函数重载
5.1函数重载的概念
什么是函数重载呢?函数重载是函数的一种特殊形式,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数的个数,类型,或者顺序)必须不同,常用来处理实现功能类似数据不同的问题。
例如在C语言中,我们想设计一个函数能够交换两个参数的数值,我们就需要根据不同的参数类型进行不同的Swap函数设计。这样就会出现很多功能相似的不同名函数,调用过程中很麻烦,需要根据不同的参数来调用不同的函数。但是C++支持重载函数,所以我们可以将这种类似的函数设定为相同的函数名,调用过程中直接调用Swap函数即可,不用关心参数类型。
//C语言
void Swapi(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void Swapd(double* x, double* y)
{
double tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
double c = 12.4;
double d = 23.5;
Swapi(&a, &b);
Swapd(&c, &d);
return 0;
}
//C++重载函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void Swap(double* x, double* y)
{
double tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
double c = 12.4;
double d = 23.5;
Swap(&a, &b);
Swap(&c, &d);
return 0;
}
注意,重载函数规定了参数的不同,但是没有规定返回值的不同。
5.2名字修饰
那么问题来了,为什么C++支持重载而C语言不支持呢?这时我们就需要了解到C++和C语言的整个编译连接过程。我们知道任何一个可执行程序都需要经过预编译、编译、汇编、链接四个过程,在链接过程中会形成一个符号表,最终符号表会进行汇总,让每个函数名对应其所在的地址,才能进行函数调用。但是C语言中函数名在形成符号表时函数名修饰规则与C++不同,C语言的函数名修饰规则只与函数名称有关,当具有相同函数名的函数出现时,编译器无法进行区分,因此就会形成链接错误。而C++的函数名修饰规则不仅仅和函数名称有关,还和函数参数有关,因此即便函数名相同的函数,其因为参数不同就会形成不同的函数名称,在符号表汇总时会对应不同的地址,因此可以实现函数重载。
5.3 extern "C"
既然C++和C的函数命名规则不同,那我们能否让C语言生成的程序来调用C++的库,或者用C++来调用C语言生成的库呢?答案是肯定的,二者可以进行互相调用,只不过需要一个特殊的声明。
我们知道,C++是兼容C语言的,而C语言并不能兼容C++,所以我们只能让C++去按照C的风格进行编译,而不能让C去改变成C++的风格。因此,extern "C" 的用处很简单,意思就是告诉编译器将该函数按照C语言的规则来编译。
例如我们以前联系的一道括号匹配的练习题(20. 有效的括号 - 力扣(LeetCode)),这里面我们用C语言写的时候就是自己实现了一个栈,假如我们有一个现成的C语言实现的栈静态库StackClib,而我们现在需要使用C++来实现,并且调用这个C语言实现的库,那我们应该怎么做呢?很简单,我们只需要在包含该库的头文件那里使用extern "C"来进行声明即可。
当我们使用C去调用C++实现的库时,也是需要声明,只不过需要额外注意一点,C语言不认识extern "C" 该命令,我们包含C++库的头文件后,里面必然会调用extern "C"名令,此时就得需要条件编译进行区分。
6、引用
6.1引用的概念
在学习C语言的时候,我们学习过指针的概念,通过指针我们可以找到变量,来对其进行操作,但是指针使用起来往往比较麻烦,需要对地址进行解引用才能找到相应的变量,并且在面对多级指针时,总是会产生混乱。基于这种情况,C++对其进行了优化封装,引出了“引用”的概念。
引用并不是重新定义一个新变量,而是给已经存在的变量取了一个别名,编译器不会为其单独开辟空间,它与它引用的变量公用同一块内存空间。例如,水浒传中豹子头林冲,林冲是他的名字,豹子头是他的外号,两个代号都表示同一个人。
定义引用变量使用的还是"&",但与取地址并不同。
int main()
{
int a=0;
int* p=&a;//p为a的地址,&a表示取出a的地址
int& b=a;//b为a的引用,b就是a
return 0;
}
6.2引用特性
在使用引用时,必须要遵循以下规则:
(1)引用在定义时必须初始化
(2)一个变量可以有多个引用
(3)引用一旦引用一个实体,就不能引用其他实体
int main()
{
int a = 10;
int& b = a;
int& c = a;
//int& d;//不存在此种写法
cout << a << endl;
cout << b << endl;
cout << c << endl;
int x = 20;
b = x;
cout << x << endl;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
6.3使用场景
(1)做参数--输出型参数,大对象传参,提高效率
引用可以作为参数进行传递,并没有对变量进行一份临时拷贝,而是直接使用该变量的别名对其操作,提高效率。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
(2)做返回值--输出型返回对象,调用者可以修改返回对象,减少拷贝,提高效率
做返回值时要特别注意一点,那就是引用变量不能出了作用域后销毁,否则就会越界访问。
//int& count()//此种引用返回结果没有保障
//{
// int n = 0;
// n++;
// return n;
//}
int& count()//此种引用返回结果正确,n不会销毁
{
static int n = 0;
n++;
return n;
}
所以,出了函数作用域,返回对象就销毁的一定不能使用引用返回,而是用传值返回。
6.4常引用
在一些引用场景中,通常会出现const修饰的引用,此种引用便被称作常引用。
int mian()
{
int a = 0;
int& b = a;
//权限不能放大
const int c = 0;
//int& d = c;
//权限可以缩小
int e = 0;
const int& f = e;
return 0;
}
使用const修饰后的引用只有可读权限,不能更改,因此一个变量如果本身被const进行修饰,那么引用也需要const修饰,无法对其进行权限放大,但是未被const修饰的变量,其引用也可被修饰,称为权限缩小。
常引用还有些特殊用处,例如下面的代码:
int main()
{
int a = 10;
double& b = a;//错误
const double& c = a;//正确
return 0;
}
为什么加了const就是正确代码呢?
原因很简单,int类型的a变量在被double类型的变量引用时,会发生隐式类型转换,但是变化的并不是a变量本身,而是会生成一个临时变量,该临时变量是double类型,并且具有常性,因此b和c引用的都不是a,而是生成的具有常性的临时变量,如果不加const相当于对该临时变量进行了权限放大,当然是编译失败的,因此加上const就可以对其进行引用。所以a即便发生变化,c也不会变化。
6.5引用和指针的区别
我们会发现,实际上引用和指针很类似,并且引用的操作一般指针都能进行完成。那为什么还要有引用呢?很简单,指针虽更加强大,但是使用起来复杂,危险。引用虽然局限一些,但是方便简单。语言层面上引用没有额外开辟空间,指针需要开辟4字节或者8字节的空间。实际在底层实现上,引用和指针的实现方式一样。
引用与指针的不同点:
(1)引用在定义时必须进行初始化,指针可以不用。
(2)引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
(3)没有NULL引用,但有NULL指针。
(4)在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
(5)引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
(6)有多级指针,但是没有多级引用
(7)访问实体方式不同,指针需要显式解引用,引用编译器自己处理
(8)引用比指针使用起来相对更安全
7、内联函数
7.1内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
7.2内联函数的特性
内联函数是C++为弥补C语言的宏而创建的一种函数模式,内联函数兼顾有宏和函数的优点并且摒弃了宏的缺点,C语言的宏在使用时无法调试,可读性差,没有安全类型检查,因此使用内联函数代替,可以方便进行调试,增加可读性。
但是,内联函数并非加上关键字就构成了内联函数,内联函数具有独特的特性。
(1)inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
(2) inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
(3)inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
8、auto关键字
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
例如:
auto并不经常用于此种写法,auto一般用于变量类型太长时,让其自动推导。
auto并非什么情况下都能使用,auto在以下场景中不能使用。
(1)auto不能作为函数的参数
int add(auto a,auto b)
{
return a + b;
}
(2)auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
(3)为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
9、基于范围的for循环
auto经常和基于范围的for循环进行一起使用,对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中 引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围。
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for ( auto& e : arr )
{
e++;
}
for ( auto e : arr )
{
cout << e << " ";
}
cout << endl;
return 0;
}
9.1范围for循环的使用条件
(1)for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
(2)基于范围的for循环中,e是arr数组中每个元素的拷贝,修改e并不会更改arr中的元素,如果要修改,应使用引用操作。
10、nullptr
C语言中空指针为NULL,底层实际上是将其定义为0,但是有些场景并不适用,因此C++中nullptr作为关键字成为了新的空指针定义,因为nullptr实际上是(void*)0。
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;
}
因此,在后面进行空指针的引用时,我们都是用nullptr,使用时不需要包含头文件,nullptr为关键字。