C++ 入门
目录
1. 命名空间
1.1 定义
1.2 使用
1.3 std 命名空间的使用惯例
1.4 命名空间嵌套
1.5 命名空间合并
2. C++输入&输出
3. 缺省参数
3.1 缺省参数分类
3.2 注意事项
4. 函数重载
4.1 C++支持函数重载的原理
5. 引用
5.1 概念及特性
5.2 常引用
5.3 引用的使用场景
5.3.1 做参数
5.3.2 做返回值
5.4 传值与传引用的效率比较
5.5 对比引用和指针
6. 内联函数
7. auto 关键字(C++11)
7.1 类型别名思考
7.2 auto 的使用规则
7.3 不能使用 auto 的场景
8. 基于范围的 for 循环(C++11)
8.1 范围 for 循环的使用条件
9. 指针空值 nullptr (C++11)
💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
1. 命名空间
1.1 定义
C/C++中,变量、函数和类的名称默认都存在于全局作用域中。全局作用域中的标识符过多时,易导致命名冲突。
命名空间通过
namespace
关键字定义,它可以将标识符的名称进行本地化,即将标识符限制在特定的作用域内,从而有效避免命名冲突。
命名空间中可以定义变量/函数/类型。
一般开发中用项目名字做命名空间名。
namespace lyl
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
以上代码定义了一个名为lyl的命名空间,其中包含变量rand、
函数Add和自定义类型结构体。
1.2 使用
命名空间的使用有 3 种方式:
- (1)命名空间名称::成员名称
int main() { printf("%d\n", N::a); return 0; }
-
(2)using 命名空间名称::成员名称;
将特定成员引入当前作用域,之后可直接使用该成员using N::b; int main() { printf("%d\n", b); return 0; }
1.3 std 命名空间的使用惯例
std 是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中。
using namespace std; //全部展开
以上代码将标准库 std 命名空间中的所有名称引入到当前作用域,这样就可直接使用标准库中的函数、类、对象等,无需每次都加 std:: 前缀。
- 风险:代码中自定义的变量名、函数名或类名与 std 命名空间中的名称相同时,会产生命名冲突。
- 对应策略:(1)指定访问,使用 std:: 前缀;或(2)展开常用的库对象/类型等。如 using std::cout; , using std::endl; 等。
- 使用惯例:
- 日常练习为方便可直接使用 using namespace std;
- 项目开发中根据需要指定访问,或展开常用的库对象/类型等
1.4 命名空间嵌套
命名空间可以嵌套,即在一个命名空间内部定义另一个命名空间。
访问嵌套的命名空间中成员时,需要使用::来逐级指定命名空间。
#include <iostream>
using namespace std;
namespace N1
{
int a = 5;
int b = 0;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int a = 6;
int c = 0;
int d = 0;
int Sub(int left, int right)
{
return left - right;
}
}
}
int main()
{
printf("%d\n", N1::Add(2, 1));
printf("%d\n", N1::a);
printf("%d\n", N1::N2::a);
return 0;
}
1.5 命名空间合并
同一个工程中,允许存在多个相同名称的命名空间。编译器在处理时会将这些命名空间合并为一个,其中的成员会被整合在一起。
// test.h
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
// test.cpp
namespace N1
{
int Add(int left, int right)
{
return left + right;
}
}
- 类的声明放在 .h 文件中,利用预处理器指令防止重复包含
- 类的函数成员实现放在一个 .cpp 文件里,该文件包含类的头文件,如 #include "Circle.h"
- 主函数所在的另一个 .cpp 文件包含类的头文件并使用类的对象进行操作
2. C++输入&输出
(1)cin 是标准输入流对象,它会根据变量的类型从标准输入(通常是键盘)读取相应的数据。
cout 是标准输出流对象,用于将数据输出到标准输出(通常是屏幕)。
使用cin
和cout
时,必须包含<iostream>
头文件,并使用std
命名空间。
<< 是流插入运算符, >> 是流提取运算符。
(2)C++的输入输出可以自动识别变量类型,不需像C语言使用 printf / scanf 输入输出时那样手动控制格式。
#include <iostream>
using namespace std;
int main()
{
int a = 1;
double b = 2.00;
char c = 'C';
cin >> a;
cin >> b >> c; // “依次”将读取到的数据存储到 b 和 c 中
cout << a << endl; // endl:换行符
cout << b << ' ' << c << endl; // b 、c 的值输出到屏幕时中间用空格分隔
return 0;
}
3. 缺省参数
缺省参数允许在声明或定义函数时为参数指定一个默认值。当调用该函数时,如果没有为对应的参数提供实参,则使用该参数的默认值;如果提供了实参,则使用指定的实参。
3.1 缺省参数分类
- 全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
- 半缺省参数:函数参数部分带有默认值
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
3.2 注意事项
-
半缺省参数必须从右往左依次给出:在函数声明中,若多个参数带有默认值,这些参数必须从最右边的参数开始依次向左设置默认值,不能出现中间参数有默认值而右边参数无默认值的情况。例如,
void Func(int a, int b = 20)
正确,void Func(int a = 10, int b)
错误,void Func(int a = 10, int b, int c = 20)错误。 -
函数的声明和定义中不能同时为同一个参数指定默认值,否则会导致编译错误。
-
缺省值必须是常量或全局变量:默认值不能是局部变量或函数调用的结果,因为这些值在编译时可能无法确定。
-
C语言的编译器不支持缺省参数。
4. 函数重载
函数重载指在同一作用域中可以有多个同名函数,只要它们的形参列表(参数个数、类型或类型顺序)不同。
函数重载允许使用同一个函数名处理不同类型的数据或不同数量的参数,从而提高代码的可读性和复用性。
如:
int Add(int left, int right)
{
cout << "int Add" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add" << endl;
return left + right;
}
4.1 C++支持函数重载的原理
C 语言中,函数名在编译后基本保持不变,因此不支持函数重载。
而C++通过名字修饰区分重载函数。
名字修饰:编译器根据函数名、参数类型和参数个数等信息修改函数名,使得每个重载函数在编译后的名称唯一。
5. 引用
5.1 概念及特性
引用是给已存在的变量取一个别名。
引用与原变量共享同一块内存空间,因此对引用的操作实际上就是对原变量的操作。
定义普通引用类型:
类型& 引用对象名 = 引用实体;
特性:
-
引用在定义时必须初始化为一个已存在的变量。
int& d; // 未初始化
-
同一个变量可以有多个引用(别名)。
int& b = a; int& c = a;
-
初始化引用一个实体后,不能再引用其他实体。
5.2 常引用
普通引用不能绑定到常量对象、字面常量和不同类型的对象;
而常引用(通过创建临时对象)可用于绑定常量对象、字面常量和不同类型的对象。
定义常量引用:
const 类型& 引用对象名 = 引用实体;
void TestConstRef()
{
const int a = 10; // a是常量对象
const int& ra = a; // 常引用绑定到常量对象
// 字面常量是临时的、不可修改的值
const int& b = 10; // 常引用绑定到字面常量
double d = 12.34;
const int& rd = d; // 常引用绑定到不同类型的对象
// 当用常量引用绑定不同类型的对象时,编译器会创建一个临时的 int 类型对象,
// 将双精度浮点数实体的值进行截断(去掉小数部分)后赋值给临时对象,
// 然后让常量引用绑定这个临时对象。
// 但实际上引用的是临时对象,而不是实体本身。
}
5.3 引用的使用场景
5.3.1 做参数
引用作为函数参数可直接操作实参,避免了值传递的拷贝开销。
例如:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
5.3.2 做返回值
如果函数返回时,返回对象还在其作用域内(未被销毁),则可以使用引用返回。引用本质上是对象的别名,若对象已被销毁,引用就会指向一个无效的内存地址,从而导致未定义行为,则必须使用传值返回。
例如:
int& Count()
{
static int n = 0;
// static 修饰的局部变量在程序的整个生命周期内存在,不随函数调用结束而销毁
n++;
return n;
}
// 错误示例:
//int& Count()
//{
// int n = 0;
// n++;
// return n;
//}
5.4 传值与传引用的效率比较
值作为参数时函数传递实参,值作为返回值时函数返回变量的临时拷贝,效率较低,尤其是参数或返回值类型较大时。而引用传递直接操作实参,效率高得多。
5.5 对比引用和指针
对比项 | 引用 | 指针 |
---|---|---|
语法概念 | 变量的别名,无独立空间,与引用实体共用空间 | 存储变量的地址 |
定义和初始化 | 定义时必须初始化 | 无相同要求 |
引用的唯一性 | 初始化后不能再引用其他实体 | 可随时指向同类型实体 |
空值处理 | 无 NULL 引用 | 有 NULL 指针 |
sizeof 结果 | 引用类型的大小 | 地址空间所占字节数(32 位平台下为 4 字节) |
自增操作 | 引用自加即引用实体加 1 | 指针自加则向后偏移一个类型大小 |
多级情况 | 无多级引用 | 有多级指针 |
访问方式 | 由编译器自动处理 | 需显式解引用 |
安全性 | 使用相对更安全 | 相对而言安全性稍低 |
6. 内联函数
内联函数是以
inline
修饰的函数。
编译时会在调用内联函数处展开函数体,避免函数调用建立栈帧的开销,从而提高程序运行效率。
特性:
- 空间换时间:若编译器将函数作为内联函数处理,会用函数体替换函数调用,可能使目标文件变大,但减少了调用开销。
- 编译器建议:
inline
只是给编译器的建议,不同编译器关于inline的实现机制可能不同。一般建议对规模小、非递归且频繁调用的函数使用inline
修饰。 - 声明和定义分离问题:不建议内联函数声明和定义分离,否则会导致链接错误,因为内联函数展开后无函数地址。
7. auto 关键字(C++11)
7.1 类型别名思考
随着程序复杂度增加,类型可能很长难于拼写,再者含义不明确导致易出错,
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
"橙子" },{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
}
return 0;
}
使用 typedef
可简化代码,
#include <string>
#include <map>
typedef std::map<std::string, std::string> Map;
int main()
{
Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
Map::iterator it = m.begin();
while (it != m.end())
{
}
return 0;
}
但在某些情况下仍有局限性。
编程中常将表达式的结果赋给变量,这就需要在声明变量时精准知晓表达式的类型。但实际中,确定表达式的类型并非易事。为了解决这一问题,C++11 为 auto
关键字赋予了全新的意义。
C++11 中,
auto
不再是存储类型指示符,而是类型指示符,编译器根据初始化表达式推导auto
声明的变量的实际类型。
为了避免与 C++98 中的 auto 混淆,C++11 只保留了 auto 作为类型指示符的用法。
auto 在实际中最常见的优势用法:跟 C++11 提供的新式for循环,及 lambda 表达式等配合使用。
7.2 auto 的使用规则
- 使用
auto
定义变量时必须初始化。 - auto 与指针 / 引用结合使用:用 auto 声明指针类型时,
auto
和auto*
效果相同;用 auto 声明引用时必须加&
。
#include <iostream>
#include <typeinfo> // 包含 typeinfo 库,用于使用 typeid 操作符
using namespace std;
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
// 使用 typeid 操作符获取变量 a 的类型信息,并将其名称输出到控制台
// typeid(a).name() 返回变量 a 的类型名称
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
分析:
(1)auto 结合指针使用,a、b 被推导为 int* 类型。另外,在 64 位系统下,Visual Studio 为了明确指针是 64 位的,会在类型输出中添加 __ptr64
标识。
(2)auto 结合引用使用,c 被推导为 int& 类型,即 x 的引用。但 typeid
操作符返回的是被引用对象的类型,而不是引用类型本身。
- 同一行定义多个变量时:同一行声明的多个变量类型必须相同,编译器只推导第一个变量类型并用于定义其他变量。
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败
7.3 不能使用 auto 的场景
- auto 不能作为函数参数。
auto
作函数参数时,编译器没有足够的上下文信息来推导其具体类型。
void func(auto a)
{
}
int amin()
{
func(num);
return 0;
}
auto
无法用于直接声明数组。
// C++ 里,数组的大小是类型的一部分,是在编译阶段必须明确确定的
void TestAuto()
{
int a[] = {1,2,3};
// 编译器能据初始化列表确定数组大小,其类型是 “含 3 个 int 元素的数组”
auto b[] = {4,5,6};
// auto 能知晓元素类型,却无法将数组大小信息融入推导类型
// 即不能自动推出 “含 3 个 int 元素的数组” 这一“完整类型”,该语句编译出错
}
8. 基于范围的 for 循环(C++11)
遍历有范围的集合(具有明确起始和结束边界,元素位置相对固定且可按顺序逐个访问的一组数据,像数组、各类标准库容器都属于此类集合)时,往往需要手动设置循环变量来控制起始和结束位置,还要进行索引计算等操作。
C++11 引入的范围 for 循环,简化了有范围集合的遍历过程。
范围 for 循环的语法:
for (范围内用于迭代的变量 : 被迭代范围) { ... }
可使用 continue
结束本次循环,break
跳出整个循环。
8.1 范围 for 循环的使用条件
- 迭代范围确定:对于数组,需明确第一个和最后一个元素的范围;对于类,需提供
begin
和end
的方法。
数组的范围 for 循环:
// 这里虽未显式地指定数组的第一个和最后一个元素的索引,
// 但编译器能够根据数组的定义自动明确迭代范围
#include <iostream>
using namespace std;
int main()
{
int numbers[] = { 1, 2, 3, 4, 5 };
for (int num : numbers)
{
if (num == 3)
{
continue;
}
if (num == 4)
{
break;
}
cout << num << " ";
}
cout << endl;
return 0;
}
#include <iostream>
using namespace std;
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
// 使用引用类型的变量 e
// 这里的 & 表示 e 是数组元素的引用,因此对 e 的修改会直接影响到数组元素
for (auto& e : array)
{
// 每个元素乘以 2
e *= 2;
}
// 使用非引用类型的变量 e
// 这里的 e 是数组元素的副本,不修改数组元素
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
TestFor();
return 0;
}
类的范围 for 循环:
#include <iostream>
using namespace std;
// 自定义一个类,用于存储一组整数
class MyIntContainer
{
private:
int data[5] = { 1, 2, 3, 4, 5 };
public:
// 返回指向迭代范围起始位置的指针,即返回起始地址
int* begin()
{
return data;
}
// 返回指向迭代范围结束位置的指针
int* end()
{
return data + 5;
}
};
int main()
{
MyIntContainer container;
for (int value : container)
{
cout << value << " ";
}
cout << endl;
return 0;
}
for 循环执行过程:
编译器调用 container.begin()
得到起始位置的指针,调用 container.end()
得到结束位置的指针。
循环从起始位置开始,依次将 data
数组中的元素赋值给变量 value
,并执行循环体中的代码 cout << value << " ";
,直到到达结束位置。
- 迭代对象支持操作:迭代对象需实现
++
和==
操作。
9. 指针空值 nullptr (C++11)
当指针没有合法的指向时,通常会使用 NULL
对其进行初始化。
NULL
本质上是一个宏,在传统的 C 头文件stddef.h
中,它有两种可能的定义:(1)字面常量 0(2)无类型指针(void*)
常量。
这种双重定义在实际使用中会带来一些问题。为提高代码健壮性,避免因NULL
的二义性导致的潜在错误,在后续编程中,表示指针空值时建议最好使用 nullptr
。
- 使用
nullptr
表示指针空值时,不需包含头文件,因为它是作为新关键字直接引入到 C++11 标准中的。 - C++11 中,
sizeof(nullptr)
与sizeof((void*)0)
所占字节数相同,这保证了nullptr
在内存占用上与传统的空指针表示方式具有一致性。