从零开始的C++学习生活 1:命名空间,缺省函数,函数重载,引用,内联函数
前言
我们说C和C++,因为C++和C其实是一家的,只是C++是C的拓展而已。
因此C++兼容C的大部分语法内容,并且新增了许多新功能。因此这个C++全流程学习博客建立在你已经学会了C语言的基础上,探讨C++新增的内容。
学海无涯,求知当以勤勉为舟。愿你在求知的路上持之以恒,不断精进。
1. C++简介与发展历程
C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不 同的地方可能有差异)在贝尔实验室从事计算机科学和软件⼯程的研究⼯作。⾯对项目中复杂的软件开 发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语言(如C语言)在表达能⼒、可维护性 和可扩展性方面的不⾜。
1983年,BjarneStroustrup在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形, 此时的C++已经有了类、封装、继承等核⼼概念,为后来的⾯向对象编程奠定了基础。这⼀年该语⾔被 正式命名为C++。
1.1 C++发展历程
时间 | 版本 | 主要特性 |
---|---|---|
1998 | C++98 | 第一个官方标准,引入STL |
2003 | C++03 | 修复错误,提高稳定性 |
2011 | C++11 | 革命性更新:lambda、智能指针、移动语义等 |
2014 | C++14 | 对C++11的扩展和改进 |
2017 | C++17 | 引入折叠表达式、结构化绑定等 |
2020 | C++20 | 重要里程碑:概念、协程、模块等 |
2023 | C++23 | 进一步完善现有特性 |
1.2. 为什么学习C++?
C++的重要性
性能卓越:接近硬件层,执行效率高
应用广泛:操作系统、游戏引擎、嵌入式系统、金融服务等
承上启下:理解计算机系统的绝佳语言
不可替代:在性能敏感领域无可替代
C++在工作领域中的应用
2. 命名空间
命名空间的概念
在C语言中有自己的库函数和语法中的关键字,我们无法使用这些库函数来作为自己变量的名字,例如无法使用main,int,rand,strlen来作为我们创建的变量的名字。
#include <stdio.h> #include <stdlib.h>int rand = 10;int main(){// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数” printf("%d\n", rand);return 0;}
C++中便解决了这一个问题。我们引入命名空间-namespace
• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中 即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下 面的rand和main不在冲突了。
namespace test {int rand = 10;int Add(int x = 1, int y = 2){return x + y;}int main = 20;
}
当然命名空间内部也可以嵌套
namespace test {namespace test2 {int strlen = 5;}int rand = 10;int Add(int x = 1, int y = 2){return x + y;}int main = 20;
}
我们定义了一个命名空间,现在我们就要使用命名空间中的变量了
如果我们要访问命名空间中的变量,那么就要使用 :: 操作符
用命名空间名字::变量名字来访问
int main()
{test::rand += 1;cout << test::rand << endl;cout << test::test2::strlen << endl;return 0;
}
值得一提的是,上述代码的cout<< <<endl为C++特有的输入代码,和C语言中的printf功能几乎一样,都代表标准输入,即往控制台中输入数据,末尾的endl代表自动换行并刷新缓冲区
命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以 下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
namespace test{int a = 3;
}using test::a;//展开test中的aint main()
{printf("%d", a);//展开后无需使用::操作符访问
}
当然也可以展开一整个命名空间
namespace test{int a = 3;int b = 5;
}using namespace test;//展开testint main()
{printf("%d", a);printf("%d", b);
}
3. 缺省函数
缺省函数的使用
C语言中我们声明的函数中只能用几个形参来接受实参的数据,但无法初始化形参的值
但在C++中我们可以初始化形参的值
int Add(int x, int y)//C语言
{return x + y;
}int Add(int x = 1, int y = 2)//C++
{return x + y;
}
在C++调用函数时,如果没有给函数传递参数,那么就会使用默认值
int main()
{cout << Add(1, 2) << endl;cout << Add(3) << endl;return 0;
}
全缺省函数和半缺省函数
全缺省函数指所有的参数都有默认值,而半缺省函数的参数从左到右中,左边的参数没有默认值,而右边的参数有默认值
//
全缺省void Func1(int a = 10, int b = 20, int c = 30){cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;}//
半缺省void Func2(int a, int b = 10, int c = 20){cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;}
4. 函数重载
我们在C语言中无法创造两个函数名一样的函数,即使他们的参数和返回值都不同
而我们在C++却可以创造函数名相同但参数一定不同的几种函数,这些函数称为重载函数
函数重载的使用
// 1、参数类型不同int Add(int left, int right){cout << "int Add(int left, int right)" << endl;return left + right;}double Add(double left, double right){cout << "double Add(double left, double right)" << endl;return left + right;}// 2、参数个数不同void f(){cout << "f()" << endl;}void f(int a){cout << "f(int a)" << endl;}// 3、参数类型顺序不同void f(int a, char b){cout << "f(int a,char b)" << endl;}void f(char b, int a){cout << "f(char b, int a)" << endl;}
注意:重载的函数要求是函数名相同而参数不同,不考虑返回值,无论返回值相同还是不同都不在重载函数的判断范围内
在使用重载函数时,系统会自动判断传递的参数类型。如果是整型就调用传递整型的函数,如果是浮点数就调用传递浮点数的函数
int Add(int x = 1, int y = 2)
{return x + y;
}double Add(double x = 1, double y = 2.2)
{return x + y;
}int main()
{cout << Add(1, 2) << endl;cout << Add(3.3) << endl;return 0;
}
注意事项
// 错误示例:返回值不同不能构成重载
// int GetValue() { return 1; }
// double GetValue() { return 1.0; } // 编译错误!// 有歧义的重载
void Func(int a = 10) {cout << "Func(int a)" << endl;
}void Func() {cout << "Func()" << endl;
}int main() {// Func(); // 错误:有歧义,不知道调用哪个版本Func(5); // 正确:明确调用带参数的版本return 0;
}
5. 引用
引⽤不是新定义⼀个变量,而是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同⼀块内存空间。比如:水壶传中李逵,宋江叫"铁牛",江湖上⼈称"黑旋风";林冲,外号豹子头;
#include <iostream>
using namespace std;int main() {int a = 10;// 引用:变量的别名int& ref = a; // ref是a的别名int& ref2 = ref; // ref2也是a的别名cout << "a = " << a << endl; // 10cout << "ref = " << ref << endl; // 10cout << "ref2 = " << ref2 << endl; // 10ref = 20;cout << "修改后 a = " << a << endl; // 20// 地址相同cout << "&a = " << &a << endl;cout << "&ref = " << &ref << endl;cout << "&ref2 = " << &ref2 << endl;return 0;
}
在上面,其实就可以可以把ref看作是a,ref2看作是ref,那么ref2其实就是a。就跟鲁迅和周树人的关系,ref是鲁迅,a是周树人。所以如果ref变了a也会变
引用的特性
引用在定义时必须初始化
⼀个变量可以有多个引用
引用⼀旦引用⼀个实体,再不能引用其他实体
#include <iostream>
using namespace std;int main() {int a = 10;// 1. 引用必须初始化// int& ref; // 错误:必须初始化int& ref = a; // 正确// 2. 引用不能改变指向int b = 20;ref = b; // 这是赋值,不是改变引用指向!cout << "a = " << a << endl; // 20(值被改变)cout << "&ref = " << &ref << endl; // 仍然是a的地址return 0;
}
引用的使用
• 引用在实践中主要是于引用传参和引用做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被 引用对象。
• 引用传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
• 引用返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节 中会继续深⼊讲解。
• 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他 语⾔的引用(如Java)是有很⼤的区别的,除了用法,最⼤的点,C++引⽤定义后不能改变指向, Java的引用可以改变指向。
• ⼀些主要用C代码实现版本数据结构教材中,使⽤C++引用替代指针传参,⽬的是简化程序,避开 复杂的指针,但是很多同学没学过引用,导致⼀头雾⽔。
#include <iostream>
using namespace std;// 引用传参:避免拷贝,可以修改实参
void Swap(int& a, int& b) {int temp = a;a = b;b = temp;
}// 引用返回:避免拷贝返回值
int& GetMax(int& a, int& b) {return a > b ? a : b;
}int main() {int x = 5, y = 10;cout << "交换前: x=" << x << ", y=" << y << endl;Swap(x, y);cout << "交换后: x=" << x << ", y=" << y << endl;// 引用返回的应用GetMax(x, y) = 100; // 修改较大的值cout << "修改后: x=" << x << ", y=" << y << endl;return 0;
}
const引用
我们也可以把具有常属性的值创造一个引用,如3.14,a+b这些。然而具有常属性变量的值必须得使用const引用,因为const修饰的引用也具有常属性,无法被更改,根常变量相吻合。如果不用const修饰那么就代表可以更改,与常属性相悖
#include <iostream>
using namespace std;int main() {const int a = 10;// const引用const int& ref1 = a; // 正确// int& ref2 = a; // 错误:权限放大int b = 20;const int& ref3 = b; // 正确:权限缩小// 临时对象的引用必须是constconst int& ref4 = 30; // 正确const int& ref5 = b * 2; // 正确// int& ref6 = 30; // 错误double d = 3.14;const int& ref7 = d; // 正确:类型转换产生临时对象return 0;
}
指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功 能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。 • 引用可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引用结果为引用类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
6. 内联函数
在C语言定义的宏虽然好用,但往往坑较多
#define Add(x,y) x+y;
这个宏有什么错误?那错误可多了,首先后面的分号就不可取,不然会直接copy到目标代码行中
其次如果目标代码行为Add(1,2)*4,那么就会替换为1+2*4,与我们的预想不一样
为了完美地替代宏,C++就引入了内联函数inline
• inline对于编译器而言只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展 开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁 调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数。
• vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下 以下两个地⽅。
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地 址,链接时会出现报错。
#include <iostream>
using namespace std;// 内联函数:建议编译器在调用处展开
inline int Add(int a, int b) {return a + b;
}// 对比宏函数
#define ADD_MACRO(a, b) ((a) + (b))int main() {int x = 5, y = 3;cout << "内联函数: " << Add(x, y) << endl;cout << "宏函数: " << ADD_MACRO(x, y) << endl;// 查看汇编代码可以发现内联函数可能被展开int result = Add(x, y);return 0;
}
内联函数只是一个建议,建议编译器把这个函数当作内联函数处理,但如果你的函数的代码行过多,甚至含有大量的递归,那么编译器就不会使用内联函数而是当作正常函数使用
// 内联函数适合短小的频繁调用的函数
// 不适合复杂函数或递归函数// 正确:短小简单
inline int Max(int a, int b) {return a > b ? a : b;
}// 可能不会被内联:函数体较大
inline void ProcessData(int* data, int size) {// 复杂的处理逻辑...for (int i = 0; i < size; ++i) {// 很多操作...}
}
7. nullptr
在C++中NULL不再代表空指针,而是0,取而代之的是nullptr
#include <iostream>
using namespace std;void Func(int x) {cout << "调用整型版本: " << x << endl;
}void Func(int* ptr) {if (ptr == nullptr) {cout << "调用指针版本: 空指针" << endl;} else {cout << "调用指针版本: " << *ptr << endl;}
}int main() {int value = 10;Func(0); // 调用整型版本Func(NULL); // 可能调用整型版本(取决于NULL的定义)Func(&value); // 调用指针版本Func(nullptr); // 明确调用指针版本return 0;
}
nullptr的优势
#include <iostream>
using namespace std;int main() {// 传统NULL的问题int* p1 = NULL; // NULL可能是0或(void*)0// C++11的nullptrint* p2 = nullptr; // 明确的空指针// 类型安全// int x = nullptr; // 错误:不能将nullptr转换为intif (p2 == nullptr) {cout << "p2是空指针" << endl;}// 与所有指针类型兼容double* pd = nullptr;char* pc = nullptr;return 0;
}
8. 综合示例
简单的学生管理系统
#include <iostream>
#include <string>
using namespace std;namespace StudentSystem {struct Student {string name;int age;double score;};// 使用引用避免拷贝void PrintStudent(const Student& stu) {cout << "姓名: " << stu.name << ", 年龄: " << stu.age << ", 分数: " << stu.score << endl;}// 函数重载void UpdateScore(Student& stu, double newScore) {stu.score = newScore;}void UpdateScore(Student& stu, double newScore, string comment) {stu.score = newScore;cout << "更新说明: " << comment << endl;}// 返回引用以便链式调用Student& InitStudent(Student& stu, string name = "未知", int age = 0, double score = 0.0) {stu.name = name;stu.age = age;stu.score = score;return stu;}
}int main() {using namespace StudentSystem;Student s1, s2;// 使用缺省参数InitStudent(s1);InitStudent(s2, "张三", 20, 85.5);PrintStudent(s1);PrintStudent(s2);// 函数重载UpdateScore(s1, 90.0);UpdateScore(s2, 95.0, "期末考试优秀");PrintStudent(s1);PrintStudent(s2);return 0;
}