目录
什么是C++呢?
先官方的解释一下:
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。
为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。
因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
简而言之:
C++就是本贾尼大佬看不惯C语言,想要创造一门用起来更爽的语言,因此创造了C++。
同时C++也是包含C语言的,因此C语言的所有语法在C++都能使用。
看这位大佬的发量,就大体明白,C++也不是容易搞明白的,因此本文将带你利用C语言的基础,走进C++。
C++的发展史
虽然本贾尼是C++之父,但是C++的发展却不只是牺牲了这位大佬的“发际线”,当然还有其他大佬……
- C with classes(C++前身):引入了类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等特性。
- C++1.0:增加了虚函数概念,支持函数和运算符的重载,以及引用和常量等。
- C++2.0:提供了面向对象编程的支持,包括保护成员、多重继承、对象的初始化和抽象类、静态成员等。
- C++3.0:进一步完善了面向对象特性,引入了模板技术来解决多重继承产生的二义性问题。
- C++98:作为第一个官方发布的C++标准版本,得到了广泛的支持,并引入了STL(标准模板库)。
- C++03:对语言特性进行了小幅修订,主要是为了减少多异性。
- C++05:引入了TR1,正式更名为C++0x,增加了许多新特性,如正则表达式、基于范围的for循环等。
- C++11:引入了许多革命性的新特性,如lambda表达式、auto类型推导、标准线程库等,使C++更像一门现代语言。
- C++14:修复了C++11中的漏洞,并增加了新的特性,如泛型lambda表达式、二进制字面常量等。
- C++17:在C++11的基础上做了改进,增加了19个新特性,包括static_assert()、Fold表达式等。
- C++20:这是自C++11以来最大的发行版,引入了模块、协程、范围、概念等特性,并对已有特性进行了更新。
- C++23:目前正在制定中,预计会进一步扩展和优化C++语言的功能。
总的来说,C++语言从最初的C with classes发展到现在的C++23,历经多次迭代和更新,不断丰富和完善其功能,使其成为了一门强大而灵活的高级编程语言。
为什么要学习C++
C++是目前市面上使用最多的语言之一,C/C++一直在语言界稳居前三。
对于操作系统、嵌入式开发、服务端开发、游戏开发,都需要大量的使用C++,因此学习C++非常有必要。
言归正传,下面进入C++的正式学习。
开篇就提到,C++相比于C语言是用起来很“爽”的语言,为什么真么说呢,主要是C++补充了C语言的一些语法,可以理解为:
多了一些吃前来很香的“语法糖”。
语法糖一:命名空间
当我们写一些大型的C语言项目时,大量的变量命名常常会搞得我们头昏脑胀,不会命名、命名冲突,导致语法错误频出,C++便引入了第一个语法糖:命名空间。
对于C语言的函数,变量的命名我们已经很熟悉,那么C++是如何命名的呢?答案是借助一个关键字:namespace。namespace顾名思义就是命名空间的意思,因此namespace可以创建一个命名空间。
举例:
namespace XXX
{
int x = 0;
double y = 0.0;
}
为了防止命名冲突,我们将变x、y放在命名空间XXX中。需要注意的是,一个独立的命名空间,花括号的尾部不能有分号!
命名空间有个强大的功能
在C++中,命名空间(namespace)是一种将代码组织成逻辑块的机制,它可以包含类、函数、变量、模板、类型别名等。命名空间的主要目的是提供一种组织代码的方式,以避免命名冲突,尤其是在大型项目中或者当多个库被一起使用时。
类、函数、变量、模板、类型别名,都可以包含在命名空间中。同时命名空间可以套用命名空间。
如何使用
有了命名空间之后,我们该如何使用命名空间的内容呢?
namespace Test
{
int test = 10;
int Add(int a, int b)
{
return a + b;
}
}
我们依靠的是一个操作符:域作用限定符 ::
为什么是域作用限定符呢?“域”即是区域,只要被花括号{ }括起来的空间,都可以看作一块域,访问命名空间,需要使用 ::限定符。
三大使用方式
以Test为例
方式一:将命名空间全部展开。
展开是一种授权,授权可以去对应的命名空间查找内部成员。具体实现为using namespace Test;
namespace Test
{
int test = 10;
int Add(int a, int b)
{
return a + b;
}
}
using namespace Test; //展开命名空间的所有元素
int main()
{
printf("%d\n", test);
return 0;
}
test变量属于Test命名空间,当展开Test命名空间之后,就可以访问内部成员。
但是,将命名空间全部展开也是一种危险的行为
namespace Test
{
int test = 10;
int Add(int a, int b)
{
return a + b;
}
}
int test = 81;
using namespace Test;
见此代码,当test函数在全局与Test中同时定义时,就会出现命名冲突。因此无论哪种展开方式(包含下面的方案),展开都不应该出现命名冲突。
方法二:将部分成员展开。
具体实现是using Test :: test;表示展开Test空间中的test变量。using + 空间名字 :: 成员名字。此时仍然可以直接访问test。
方法三:直接在使用处展开。
格式: 命名空间 :: 成员
namespace Test
{
int test = 10;
int Add(int a, int b)
{
return a + b;
}
}
// ::域作用限定符
int test = 81;
int main()
{
printf("%d\n", Test::test);
return 0;
}
虽然出现两个test,但是在使用处直接引用,就可以限定去Test空间寻找test变量,而不是去全局。
在工程中,常用的符号一般采用第二种展开方式,不常用的用第三种。
对于C++,有一个标准命名空间,叫做std,常用的函数都在std中。
在C++中,输出用cout,输入用cin,换行用endl,这些都定义在std中。
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main()
{
int i = 0;
cin >> i;
cout << i << endl;
return 0;
}
<< 流插入运算符 , >>是流提取运算符
那为什么引用头文件之后,还得展开命名空间呢?
下面是比较标准的解释:
在C++中,cout 是 iostream 库 中定义的一个对象,用于输出数据到标准输出(通常是终端或控制台)。iostream 库定义在 std 命名空间中。命名空间是一种将库名字封装起来的方法,以避免不同库中可能存在的名字冲突。
当您在程序中包含 <iostream> 头文件时,您实际上是包含了 iostream 库的所有声明。然而,默认情况下,这些声明都是在 std 命名空间之内的。因此,如果您直接使用 cout 而不指明它属于 std 命名空间,编译器将不知道您所指的 cout 是 std 命名空间中的 cout。
语法糖二:缺省参数
缺省是默认的意思,缺省参数就是指默认的参数,对于C++函数,可以给形参创建一些缺省值,使之成为缺省参数。
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
void Date(int year = 2024, int month = 5, int day = 9)
{
cout << year << " " << month << " " << day << endl;
}
int main()
{
Date(2022, 11, 13);
Date();
return 0;
}
对于Date函数,给定了一些缺省参数。
程序运行结果:
2022 11 13
2024 5 9
当传参时,打印结果是参数结果;不传参时,打印结果是缺省结果。
同时缺省还分为全缺省和半缺省。全缺省是指将所有参数都设置为缺省参数;半缺省是指将部分参数设置为缺省参数,但是半缺省有一定规则:参数必须从右往左缺省,且中间不能跳过参数。
多提一嘴,函数的定义应该声明与定义分离,如果定义在头文件中,那包含头文件时,会同时在符号表存在多个函数符号,出现符号冲突,连接错误。
缺省函数应该在声明时给出缺省参数,而不是定义时,原因如下:
缺省参数通常在函数原型(函数声明)中给出,而不是在函数定义中。函数原型通常位于头文件中,而函数定义位于源文件中。这样做的好处是,你可以确保无论函数在哪里被调用,调用者都能看到缺省参数的值。
总结一些缺省的注意点:
void Func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
语法糖三:函数重载
函数重载真是C++最甜的语法糖之一了,通过对比C与C++,就可以得知。
讲解函数重载之前,先讲一个笑话,这个笑话就体现了函数重载的思想。
NBA的球员,Lebron曾经在一场比赛中投出10中9的优秀命中率,因此球迷乐呵到“出手十次,一次不进”的笑话。大家可以细细品一品“一次不进”这句话。
void SwapInt(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void SwapChar(char* pa, char* pb)
{
char tmp = *pa;
*pa = *pb;
*pb = tmp;
}
在使用时:
int main()
{
int a = 10;
int b = 2;
SwapInt(&a, &b);
char m = 'a';
char f = 'b';
SwapChar(&m, &f);
return 0;
}
C++对于swap的实现:
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void Swap(char* pa, char* pb)
{
char tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 2;
Swap(&a, &b);
cout << a << " " << b << endl;
char m = 'a';
char f = 'b';
Swap(&m, &f);
cout << m << " " << f << endl;
return 0;
}
可以看到,可以直接使用相同的函数名字,对于不同的参数,函数可以自己去匹配类型。
但是注意函数重载与函数缺省参数同时使用时的冲突问题。
那为什么C不支持函数重载,但是C++支持呢?
那就要从程序的运行阶段去分析了。程序的运行阶段分为:与处理、编译、汇编、链接,四个阶段。
在编译时,会进行符号汇总;汇编会生成符号表;链接会对符号表进行合并与重定位。对于C语言,函数名在生成符号时,往往只是直接利用函数名作为符号,或者只是进行简单的修饰,因此函数名不能冲突;但是C++却有一套“独特的命名规则”,这种命名规则叫做名字修饰(name Mangling)。同时名字修饰也是支持重载的原理。
在Linux下,gcc编译器可以很明显的观察到函数符号的名字修饰规则:
【_Z+函数长度+函数名+类型首字母】
因此构成函数重载时,“类型首字母”的不同,也构成了符号表中符号的不同,不至于在链接阶段出现链接错误。
语法糖四:引用
先解释下何为引用,引用就是“取别名”。举个例子,国足因为在球场的拉跨表现,被球迷人称为“海参队”,海参队就是球迷们对国足的别称。
int main()
{
int a = 10;
int& b = a;
cout << b << endl;
return 0;
}
b就是a的一个引用变量,可以理解为b就是a的“别名”,对b进行操作,就可以控制a。
int main()
{
int a = 10;
int& b = a;
++b;
cout << a << endl; //打印11
return 0;
}
int& b = a;
const int& b = a;
const int& b = a;
Ⅱ
int a = 0;
int func()
{
a = 3;
return a;
}
int main()
{
int b = func();
return 0;
}
这段代码的意思是用b接收返回值,b接收的是数值的拷贝。
int a = 0;
int func()
{
a = 3;
return a;
}
int main()
{
const int& b = func();
return 0;
}
这段代码表示用b作为引用变量,接收函数的返回值,引用变量b是a的一个别称,可以指向这块空间。但是为什么b需要被const修饰呢?
这是因为函数在返回的时候,会将返回值传递给一个临时的对象,但是引用不能直接绑定到临时对象上(临时对象的生命周期极短),为了防止引用悬空,通常会将引用变量用const修饰,此时这个临时对象的生命周期也会变长。
引用变量的两种应用
引用传参
引用传参是指用引用变量作为形参,接收实参。再拿Swap函数举例。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 3, b = 5;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
既然引用变量只是一个别名,可以指向“本体”,那边可以用引用变量接收形参,进而修改实参。
引用返回
引用返回是利用引用变量接收引用返回。
先看一段传值返回的代码
int Func()
{
int n = 3;
return n;
}
int main()
{
int a = Func();
return 0;
}
a的值是多少呢?a = 3,其过程是,先将n传给一个临时的对象,a再接收临时对象的值。
传值返回用值接收
再看引用返回的代码
int& Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int& a = Add(3, 5);
Add(2, 2);
cout << a << endl; // 打印 4
return 0;
}
引用返回是用引用变量接收函数的返回值(返回类型之后需要加一个&,表示引用返回),传引用返回用引用变量接收
C++中int& 函数利用引用返回时,只能使用引用变量接收吗
在C++中,使用引用作为函数返回类型时,通常情况下,返回的是某个变量的别名,而不是一个独立的变量。因此,通常你确实需要使用一个引用变量来接收这个返回值,以便于直接对原始变量进行操作。
因此,在使用引用返回时,一定要注意返回变量会不会出函数留存。
引用传参与引用返回的优点
引用传参(何时都可以使用)
1.提高函数效率
2.输出型参数的修改(如力扣题目returnSize的修改)
引用返回(出函数对象的作用还在,方可使用)
1.提高效率
2.修改返回对象。
看到这里,你或许会疑问,怎么引用与指针如此相似,引用在语法上不会占用空间,其实引用在底层和指针都是一样的。
引用与指针好比是大众与保时捷,在很多时候,往往保时捷的发动机等配置与大众是一样的,只不过在外部的外观不一样。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
语法糖五:内联函数
C++们在使用C语言的宏时,发现了宏(宏常量、宏函数)有大量的弊端:
1.没有安全类型的检查
2.不能调试(预处理阶段被替换)
3.可维护性差
因此内联函数便继承了宏的优点:
1.具有复用性
2.不需要栈帧的建立(针对频繁调用的小函数),效率高
同时拒绝了宏的弊端,应运而生。
内联函数的概念
inline int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = Add(3, 5);
cout << a << endl; //打印8
return 0;
}
这便是一个内联函数的使用。那内联函数和普通函数有什么区别呢?当然有区别,内联函数不需要建立栈帧,而是在使用的地方直接展开。
内联函数相比于普通函数的特性
C++有哪些技术替代宏?
1. 常量定义 换用const enum
2. 短小函数定义 换用内联函数
语法糖六:auto关键字(C++11)
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl; //打印变量的类型typeid(参数).name()
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
auto的使用规则
int main()
{
int x = 10;
auto a = &x;
auto* b = &x; //等价上一行
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
cout << x << endl;
return 0;
}
2.在同一行定义多个变量
语法糖七:基于范围的for循环(C++11)
范围for的语法
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& m : array) //此处用引用变量 m ,去不断指向数组中的元素。
m *= 2;
for (auto m : array)
cout << m << " ";
cout << endl;
return 0;
}
此处必须用引用变量,才能修改数组的元素,&不能漏掉。这也是auto语法的常用场景。
范围for的强大之处是:1.自动迭代 2.自动判断结束
范围for的使用条件
语法八:指针空值nullptr(C++11)
可以看到,这并不是一颗语法糖,而是吃糖吃多了,吃出蛀牙了,此处是填坑。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif