前言
C++作为一种通用编程语言,自1980年代问世以来,凭借其强大的性能、灵活性以及对面向对象编程的支持,广泛应用于各种领域。从系统软件、游戏开发到嵌入式系统和高性能应用,C++无处不在。相比其他编程语言,C++不仅继承了C语言的底层操作能力,还提供了高层次的抽象和面向对象的设计思想,使其在复杂项目的开发中得以脱颖而出。
本篇文章旨在探讨C++初学者应该具备的入门知识以帮助你更快理解之后的类和对象。无论你是编程初学者,还是有经验的开发人员,希望通过本篇文章可以帮助你深入理解C++的独特之处。我们将从基础语法开始,逐步介绍到高级特性,并结合具体示例,以便让读者更直观地掌握C++的精髓。
🍋1、C++关键字
C++语言中有许多关键字,它们是编译器保留的,具有特定的意义,不能作为标识符使用。常见的关键字包括:
- 数据类型:
int
,char
,float
,double
,bool
,void
,enum
,class
,struct
,union
- 控制语句:
if
,else
,switch
,case
,for
,while
,do
,break
,continue
- 存储类型:
static
,extern
,register
,auto
,mutable
,thread_local
- 修饰符:
const
,volatile
,explicit
,inline
,virtual
,friend
,constexpr
,override
,final
- 异常处理:
try
,catch
,throw
- 其他:
namespace
,new
,delete
,this
,sizeof
,typedef
,typename
,using
,return
🍋2、命名空间
命名空间(namespace)用于组织代码,避免命名冲突。特别是在大型项目中,不同模块中可能会有相同的标识符,通过命名空间可以将它们区分开。
2.1 命名空间的定义
命名空间通过
namespace
关键字来定义,命名空间可以包含函数、变量、类、结构体、枚举等。其基本语法如下:namespace NamespaceName { // 命名空间内的代码 int myVar = 10; void myFunction() { std::cout << "Hello from NamespaceName" << std::endl; } }
要访问命名空间中的成员,需要使用
命名空间名::成员名
的方式:NamespaceName::myFunction(); // 访问命名空间中的函数 std::cout << NamespaceName::myVar << std::endl; // 访问命名空间中的变量
2.2 使用命名空间
C++中使用命名空间有多种方式:
1. 直接指定命名空间
通过命名空间名称和
::
作用域运算符,可以显式地访问命名空间中的成员:namespace MyNamespace { int myVar = 5; void myFunction() { std::cout << "Inside MyNamespace::myFunction" << std::endl; } } int main() { MyNamespace::myFunction(); // 使用命名空间访问函数 std::cout << MyNamespace::myVar << std::endl; // 使用命名空间访问变量 return 0; }
2.
using
声明
using
声明可以引入命名空间中的某个成员,使其可以直接使用,而无需每次都加命名空间前缀:using MyNamespace::myFunction; // 只引入某个成员 int main() { myFunction(); // 直接调用,不需要命名空间前缀 return 0; }
3.
using namespace
声明
using namespace
声明会将整个命名空间引入当前作用域,使其中的所有成员都可以直接使用:using namespace MyNamespace; // 引入整个命名空间 int main() { myFunction(); // 直接调用 std::cout << myVar << std::endl; // 直接访问变量 return 0; }
注意:虽然
using namespace
可以减少代码的复杂性,但它可能会引发命名冲突,尤其是在不同命名空间中有相同名称的成员时。
2.3 嵌套命名空间
命名空间可以嵌套使用,即一个命名空间可以包含另一个命名空间。访问嵌套命名空间中的成员时,使用嵌套的
::
来指定作用域。namespace OuterNamespace { namespace InnerNamespace { void innerFunction() { std::cout << "Inside InnerNamespace" << std::endl; } } } int main() { OuterNamespace::InnerNamespace::innerFunction(); // 访问嵌套命名空间的成员 return 0; }
从C++17开始,可以用简化的语法来定义嵌套命名空间:
namespace A::B::C { void function() {} }
这种简化语法使嵌套命名空间的定义更加清晰易读。
2.4 标准命名空间
C++标准库中的所有标识符(如
std::cout
、std::vector
等)都定义在**标准命名空间(std)**中。为了避免与用户定义的标识符冲突,C++将标准库的所有组件放入std
命名空间。#include <iostream> int main() { std::cout << "Hello, World!" << std::endl; // 使用std命名空间 return 0; }
你也可以通过
using namespace std;
来省略std::
前缀,但在大型项目中通常不推荐这样做,避免与其他命名空间中的成员冲突。
2.5 命名空间的实际应用
- 避免命名冲突:命名空间的最大作用是避免命名冲突。在大型项目或多方合作的项目中,不同开发者可能会定义相同名称的变量、类或函数。通过使用命名空间,可以确保这些定义不会冲突。
- 模块化代码:命名空间有助于组织代码,将相关功能模块分组,增强代码的可读性和维护性。例如,将数据库相关的函数放在一个命名空间中,将网络相关的函数放在另一个命名空间中。
- 与库结合使用:当使用第三方库或标准库时,命名空间有助于避免不同库中相同名称的类或函数相互冲突。
🍋3、流插入与流提取
C++使用标准输入输出流进行数据的输入和输出,主要使用
cin
和cout
。
#include <iostream> using namespace std; int main() { // 流插入运算符<< cout << "hello world" << endl; //自动识别类型 int x = 10; double d = 10.1; // 流提取运算符>> cin >> x >> d; cout << x << " " << d << endl; return 0; }
说明:
std::cout
是标准输出流,默认指向控制台。<<
是流插入运算符,将后面的数据插入到输出流中。std::endl
插入一个换行符并刷新输出缓冲区。std::cin
是标准输入流,默认从键盘获取输入。>>
是流提取运算符,将从输入流中提取的数据赋值给变量。- 流提取运算符会自动处理空格和换行符,字符串输入时只读取第一个单词(遇到空格、换行停止)。
🍋4. 缺省参数
4.1 缺省参数的概念
缺省参数是声明或定义函数为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include <iostream> using namespace std; void func(int a = 0) { cout << a << endl; } int main() { func(); // 没有传参时,使用默认值 func(10); // 传参时,使用指定的实参 return 0; }
4.2缺省参数的分类
- 全缺省参数 :
所有的参数都设置了默认值。
void func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
- 2.半缺省参数 :
部分参数设置了默认值。
void func(int a, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给。
- 缺省参数不能在函数声明和定义中同时出现,最好在声明中提供缺省参数,定义时不再提供,以避免不确定的行为。
// a.h void func(int a = 10); // a.cpp void func(int a = 20) {} // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那就无法确定到底该用哪个缺省值。
缺省参数的值必须是编译时可确定的常量表达式或全局变量。
缺省参数是C++的特性,C语言不支持此特性。
4.4 缺省参数的使用建议
- 缺省参数可以提高函数调用的灵活性,但过度使用可能降低代码的可读性。建议合理使用缺省参数,尤其是在参数数量较多时,尽量将最常用的参数放在前面。
- 当函数具有多个可选参数时,使用缺省参数可以减少函数重载的数量,简化代码结构。
🍋5. 函数重载
5.1 函数重载概念
函数重载指的是在同一作用域中可以定义多个名称相同但参数列表不同的函数。编译器根据参数的数量、类型、顺序来区分这些函数,并在调用时根据传递的参数自动选择合适的重载版本。
函数重载提供了灵活性,使得同一个函数名可以执行与参数类型或数量相关的不同操作,从而提高了代码的可读性和可维护性。
#include <iostream> using namespace std; void print(int x) { cout << "Integer: " << x << endl; } void print(double x) { cout << "Double: " << x << endl; } void print(string x) { cout << "String: " << x << endl; } int main() { print(10); // 调用 print(int) print(3.14); // 调用 print(double) print("Hello!"); // 调用 print(string) return 0; }
5.2 函数重载的规则
- 参数列表必须不同:
- 函数重载的核心规则是参数列表必须不同,即参数的数量、类型或顺序不同。如果参数列表相同,编译器无法区分它们,导致重载失败。
void func(int a); // 正确 void func(double a); // 正确,参数类型不同 void func(int a, double b); // 正确,参数数量不同
- 返回值类型不参与重载:
- 仅靠不同的返回值类型不能作为函数重载的依据,因为编译器仅通过参数匹配来决定调用哪个重载版本,而不会通过返回值来判断。
int func(int a); // 正确 double func(int a); // 错误,返回类型不同,但参数相同,无法重载
- 默认参数与重载的结合:
- 函数重载时,使用默认参数时需要注意与其他重载函数产生冲突,尤其是当默认参数使得函数签名与其他重载函数相同时,编译器可能无法区分它们。
void func(int a, int b = 5); // 默认参数 void func(int a); // 可能冲突,因为调用 func(10) 时无法区分
- 常量参数和非常量参数的重载:
- 可以为常量与非常量的形参重载函数,编译器会根据传递的参数类型选择对应的重载版本。
void func(int& a); // 非常量引用 void func(const int& a); // 常量引用
5.3 函数重载的常见用法
- 处理不同类型的输入:当相同的逻辑需要处理不同的数据类型时,可以通过函数重载来避免定义多个函数。
void print(int x) { cout << x << endl; } void print(double x) { cout << x << endl; }
- 处理不同数量的参数:函数重载可以用于处理不同数量的参数,而无需为每个不同的参数数量定义一个新的函数。
void sum(int a) { cout << a << endl; } void sum(int a, int b) { cout << a + b << endl; }
- 默认参数和函数重载的结合:通过设置默认参数,可以进一步简化函数重载的使用。
void logMessage(string message, int level = 1) { cout << "Level " << level << ": " << message << endl; }
5.4 函数重载的注意事项
- 避免模糊重载:
- 当函数参数类型之间存在隐式转换时,可能会导致编译器无法明确调用哪个重载函数,造成模糊调用。
void func(int a); // 整型 void func(double a); // 双精度型 // 调用 func(4.5f); // 可能导致编译器无法确定调用哪个函数
在这种情况下,编译器会报错,因为
4.5f
可以隐式转换为int
或double
,导致重载不明确。
- 合理使用重载与默认参数:
- 当函数重载和默认参数混合使用时,要特别注意函数调用的唯一性,避免因为默认参数导致函数签名相同而引发歧义。
- 函数指针与重载:
- 当使用函数指针时,由于函数重载可能导致歧义,因此必须显式地指定函数签名来匹配具体的重载版本。
void func(int a); void func(double a); void (*funcPtr)(int) = func; // 必须指定函数的签名以避免重载的歧义
🍋6. 引用
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一个地址。
#include <iostream> using namespace std; int main() { int a = 0; int& b = a; int& c = b; int& d = c; cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; //会发现a,b,c,d的地址一样 return 0; }
- 注意引用必须初始化。
- 一个变量可以有多个引用。
- 一个变量一旦引用一个实体,再不能引用其他实体。
6.2 引用的妙用
- 变量交换
#include <iostream> using namespace std; void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } int main() { int x = 0, y = 1; swap(x, y); cout << x << " " << y << endl; return 0; }
- 链表的二级指针
#include <iostream> using namespace std; typedef struct ListNode{ int val; struct ListNode* next; }LtNode, *PltNode; void ListPushBack(PltNode& plisthead , int x) { // ...; // plisthead = newnode; // ...; } int main() { PltNode plist = nullptr; ListPushBack(plist, 1); return 0; }
注意:此时程序编译不会出错,是由于PltNode& 等价于 ListNode**。
6.3 常引用
- 权限的平移、放大、缩小
int main() { // 不可以 // 权限不可以放大 const int a = 0; // int& b = a; // 可以,c拷贝给d,没有放大权限,因为d的改变不影响c const int c = 0; int d = c; // 可以 // 引用过程中,权限可以平移或者缩小 int x = 0; int& y = x; const int& z = x; ++x; return 0; }
- 举例
int fun1() { static int x = 0; return x; } int& fun2() { static int x = 0; return x; } int main() { // int& ret1 = func1(); // 在编译过程中,fun1返回的是一个与x值相同的临时变量,将其传给ret1 // 临时变量具有常性,此时ret1不具有常性,相当于权限放大 const int& ret1 = func1(); // 权限平移 int& ret2 = func2(); // 权限平移 const int& ret2 = func2(); //权限缩小 return 0; }
6.4 应用场景
- 做参数
- 输出型参数:形参的改变会影响实参。
- 提高效率:大对象/深拷贝类对象。
void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; }
- 引用做返回值
- 提高效率,减少拷贝。
- 具有危险性(如果在静态区不危险)。
- 修改返回值+获取返回值。
#include <iostream> #include <cstdio> using namespace std; // 错误样例 int& Count1(int x) { int n = x; n++; // ... return n; } // 正确样例 int& Count2(int x) { static int n = x; n++; // ... return n; } int main() { int& ret1 = Count1(10); // 返回的是n的别名,没有进行拷贝 cout << ret1 << endl; printf("sssssssssssss\n"); // 起到销毁栈帧的作用 cout << ret1 << endl; // 这里打印的ret的值是不准确的 // 如果Count函数结束,栈帧销毁,如果没有销毁栈帧,那么ret的结果侥幸是正确的 // 如果Count函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值 int& ret2 = Count2(20); cout << ret2 << endl; printf("sssssssssssss\n"); cout << ret2 << endl; // 这里打印的ret的值是准确的 return 0; }
总结:
- 基本任何场景都可以引用传参。
- 谨慎用引用做返回值,出了作用域,对象不在了,就不能用引用返回,还在就可以用引用返回。
6.5 引用与指针的不同点(面试常考):
- 引用的概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显示解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全。
🍋7、内联函数
内联函数(
inline
function)是C++中一种用于提高程序执行效率的机制,建议编译器将函数的调用替换为函数体本身,以减少函数调用带来的开销。与普通函数不同,内联函数在编译时将函数体嵌入到每个调用点,而不是通过常规的函数调用机制跳转到函数地址。这种机制主要用于短小、频繁调用的函数。
7.1 内联函数的定义
在函数定义前使用
inline
关键字,即表明该函数为内联函数:inline int add(int a, int b) { return a + b; } int main() { int result = add(5, 10); // 编译器可能将 add(5, 10) 替换为 5 + 10 return 0; }
7.2 内联函数的工作原理
- 普通函数调用:调用普通函数时,程序需要将当前的执行状态(如寄存器内容、返回地址等)保存到栈上,然后跳转到函数的代码位置。函数执行完后,再跳回调用点,恢复执行状态。这个过程称为函数调用开销。
- 内联函数调用:内联函数通过将函数体直接嵌入到调用点,从而消除函数调用开销。编译器在编译时会将函数调用替换为函数体,从而避免栈操作和跳转的开销。
7.3 内联函数的使用场景
内联函数适用于短小的、频繁调用的函数,特别是那些函数体代码量较少且执行简单的函数,如getter、setter等。例如:
inline void set(int& a, int value) { a = value; } inline int get(const int& a) { return a; }
7.4 内联函数的特点
- 函数体替换:编译器将在每个调用点用内联函数的函数体替换函数调用,减少跳转和栈操作。
- 消除调用开销:由于内联函数在编译阶段展开,避免了传统函数的调用开销,执行效率高。
- 优化机会有限:虽然使用
inline
提示编译器内联函数,但编译器可能不会总是内联。如果函数体较大或过于复杂,编译器可能忽略内联建议,而将函数当作普通函数处理。- 增加代码体积:由于函数体会在每个调用点展开,使用内联函数可能导致代码膨胀(增加二进制文件体积),特别是当函数体较大且频繁调用时,内联可能适得其反。
7.5 不适合作为内联函数的场景
- 递归函数:递归函数不适合内联化,因为内联函数要求在编译时展开,而递归会导致无限的展开。
- 包含循环或复杂逻辑的函数:复杂的函数不适合内联,因为展开后会使代码膨胀,失去内联的性能优势。
- 虚函数:虚函数通常不会内联,因为它们的调用是在运行时通过虚函数表(vtable)动态解析的。
7.6 内联函数的实际控制
虽然使用了
inline
关键字,但最终是否内联函数由编译器决定。编译器会根据函数的大小、复杂性和调用频率等因素自动判断是否展开函数体。inline void largeFunction() { // 大量复杂的代码 }
对于这样的函数,编译器可能不会展开它,而是将其当作普通函数处理,以避免代码膨胀。
7.7 内联函数与宏函数的区别
在C和C++中,除了内联函数,还可以使用宏来定义简单的操作。然而,内联函数和宏有本质区别。
1. 内联函数 vs 宏函数
特性 | 内联函数 | 宏函数 |
---|---|---|
定义方式 | 使用 inline 关键字 | 使用 #define 指令 |
类型检查 | 有,编译时进行类型检查 | 无,纯文本替换,无类型检查 |
副作用 | 无,参数求值一次 | 有,参数可能多次求值 |
调试 | 支持调试,可以设置断点 | 不支持调试,难以跟踪 |
作用域 | 遵循函数作用域 | 无作用域,可能引发冲突 |
性能 | 编译器决定是否内联,较安全 | 预处理阶段展开,存在潜在风险 |
2. 总结:
- 内联函数是一种优化工具,用于减少函数调用的开销,同时保持了函数的类型检查和调试功能。它比宏函数更安全、可靠,适用于简单、短小的函数。
- 宏函数虽然可以提供类似内联的效果,但由于它没有类型检查和作用域限制,容易引发难以排查的错误,应尽量避免使用,尤其在C++中,推荐使用内联函数代替宏函数。
🍋8、auto关键字(C++11)
8.1 auto在STL中的应用
auto
在使用STL(标准模板库)时非常有用,尤其是在使用迭代器时,auto
可以避免手动声明复杂的迭代器类型。#include <iostream> #include <map> #include <string> #include <vector> int main() { vector<int> v; // 类型很长 // vector<int>::iterator it = v.begin(); // 等价于 auto it = v.begin(); std::map<std::string, std::string> dict; // std::map<std::string, std::string>::iterator dit = dict.begin(); // 等价于 anto dit = dict.begin(); return ; }
8.2 auto在范围for中的应用
当我们希望直接操作容器中的元素时(例如修改元素的值),可以使用
auto&
来遍历容器中的元素引用。#include <iostream> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用 auto& 遍历,修改原容器中的元素 for (auto& element : vec) { element *= 2; // 将每个元素翻倍 } // 再次遍历,输出修改后的元素 for (auto element : vec) { std::cout << element << " "; } std::cout << std::endl; return 0; }
解释:
auto& element
表示按引用遍历,因此对element
的修改会直接作用于vec
中的元素。- 遍历后,
vec
中的所有元素都被翻倍。
8.3 使用 const auto&
防止修改
如果我们希望遍历容器,但不希望修改元素的值,可以使用
const auto&
来声明每个元素为常量引用,从而避免误修改。#include <iostream> #include <vector> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用 const auto& 遍历,避免修改容器中的元素 for (const auto& element : vec) { std::cout << element << " "; // 只能读取,不能修改 } std::cout << std::endl; return 0; }
解释:
const auto& element
表示按常量引用遍历,因此不能修改vec
中的元素,只能读取。- 这样可以避免无意中修改容器的数据,保证数据的安全性。
结语:
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!