一、C++的发展史
二、C++的版本更新
C++各个版本的主要特性如下:
三、C++在编程语言的排名
四、C++在⼯作领域中的应⽤
五、书籍推荐
六、C++的第一个程序
C++中需要把定义⽂件 代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤g++编译,不再是gcc。
#include<iostream>
using namespace std
int main()
{
cout<<"hello world\n"<<endl;
return 0;
}
七、命名空间
存在意义
在C/C++中,由于变量、函数和类的名称存在于全局作用域中,可能会导致命名冲突或名称污染的问题。为了解决这些问题,引入了命名空间(namespace)的概念。命名空间提供了一种将标识符名称本地化的机制,使得在不同的命名空间中可以使用相同的名称来定义变量、函数和类,而不会发生冲突。
具体来说,命名空间允许开发者在一个命名空间内部定义变量、函数和类,这些定义在该命名空间内部有效,不会与其他命名空间中的同名标识符发生冲突。通过使用命名空间,可以有效管理和组织代码,提高代码的可维护性和可扩展性,同时减少全局作用域中的命名冲突问题。
让我们通过具体的代码示例来说明在C语言中缺乏命名空间可能导致的问题。我们将模拟两个不同的代码文件,每个文件都定义一个名为 `calculate` 的函数,然后尝试将它们链接在一起,看看会发生什么问题。
文件1: math.c
/ math.c
#include <stdio.h>
// 定义一个名为 calculate 的函数
void calculate(int a, int b)
{
printf("Math calculate: %d + %d = %d\n", a, b, a + b);
}
文件2:finance.c
// finance.c
#include <stdio.h>
// 同样定义一个名为 calculate 的函数
void calculate(float principal, float rate, int time)
{
float interest = principal * rate * time;
printf("Finance calculate: Interest = %f\n", interest);
}
现在,我们尝试编写一个主程序来使用这两个文件中的函数:main.c
// main.c
#include <stdio.h>
// 引入 math.c 和 finance.c 中的 calculate 函数声明
void calculate(int a, int b);
void calculate(float principal, float rate, int time);
int main() {
calculate(3, 4); // 调用 math.c 中的 calculate 函数
calculate(1000.0f, 0.05f, 2); // 调用 finance.c 中的 calculate 函数
return 0;
}
在上面的例子中,我们尝试在 `main.c` 中使用 `math.c` 和 `finance.c` 中定义的 `calculate` 函数。由于这两个函数具有相同的名称但不同的参数类型和实现,编译器将无法区分它们应该调用哪一个。
这种情况会导致链接时的冲突或者编译器错误,具体取决于编译器的实现和配置。这正是缺乏命名空间概念时可能出现的问题之一:全局命名空间中的标识符冲突,因为所有函数都必须具有唯一的全局名称。
通过引入命名空间,我们可以将 `calculate` 函数分别放置在 `math` 和 `finance` 命名空间中,这样就可以避免名称冲突,使得代码更加模块化和可维护。
命名空间的定义
命名空间(namespace)在C++中提供了一种将变量、函数和类型定义本地化的机制,从而避免全局作用域中的命名冲突问题。以下是关于命名空间的几个重要概念和使用方法:
1. 定义和使用命名空间
使用 `namespace` 关键字定义命名空间,后面跟着命名空间的名称,然后在一对 `{}` 中定义命名空间的成员。例如:
namespace star//star为命名空间名
{
int one = 1;
int Add(int firstnumber, int secondnmuber)//定义函数,用作加法
{
return firstnumber + secondnmuber;
}
struct Seqlist//定义顺序表
{
int* arr;
int size;
int capacity;
};
};
int main()
{
int one = 2;
printf("%d\n", one);//此时one为全局变量
printf("%d\n",star::one);//此时one为命名空间内的变量
return 0;
}
2. 解决命名冲突:
命名空间本质上是定义了一个独立的作用域,不同的命名空间可以定义相同名称的变量、函数或类型,不会发生冲突。例如,可以在不同的命名空间中定义同名函数,如 `rand`,而它们不会相互影响。
3. 命名空间的类型:
在C++中,有多种作用域类型,包括函数局部作用域、全局作用域、命名空间作用域和类作用域。这些作用域影响编译器在查找变量、函数或类型定义时的逻辑路径,确保名字冲突得以解决。
运行结果
4. 命名空间的嵌套和多文件定义:
命名空间可以嵌套定义,允许更细粒度地组织代码。在项目中,即使在多个文件中定义了相同名称的命名空间,编译器也会将它们视为同一个命名空间,不会发生冲突。
5. 标准库中的命名空间:
C++标准库中的所有内容都放置在一个名为 `std` 的命名空间中,这样可以避免与用户代码中的命名冲突。
命名空间使⽤
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以 下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
八、C++输⼊&输出
在C++中,使用 `<iostream>` 标准库可以方便地进行输入和输出操作,以下是一些具体的用法和注意事项:
1. 输入和输出流对象:
std::cin:用于从标准输入流(键盘)读取数据。
std::cout:用于向标准输出流(屏幕)输出数据。
std::endl:用于插入换行符并刷新输出缓冲区。
2. 流插入和流提取运算符:
<<:流插入运算符,用于将数据插入到输出流中。
>>:流提取运算符,用于从输入流中提取数据。`
3. 命名空间 std:
所有与输入输出相关的类和函数都定义在 `std` 命名空间中。为了使用 `cin`、`cout`、`endl` 等,可以使用 `std::` 前缀,或者使用 `using namespace std;` 来简化代码。
4. 与 C 语言的兼容性:
<iostream>头文件隐式地包含了 `<stdio.h>`,因此即使不显式包含 `<stdio.h>`,也可以使用 `printf` 和 `scanf`。
#include<iostream>
using namespace std;
int main()
{
double a = 5.34;
float b = 23.54;
int c = 6;
char d = 'x';
cout << a << " " << b << " " << c <<" "<<d<< endl;
return 0;
}
运行结果
C++可以自动识别变量类型
int main()
{
double a = 0;
float b = 0;
int c = 0;
char d = 'x';
cin >> a >> b >> c >> d ;
cout << a <<" " << b << " " << c << " " << d << " " << endl;
return 0;
}
运行结果
特别说明
int main()
{
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
九、缺省参数
在C++中,缺省参数(或称为默认参数)允许在声明或定义函数时为函数的参数指定一个默认值。在函数调用时,如果没有显式提供对应参数的实参,则使用该参数的默认值;如果提供了实参,则使用提供的实参值。
-
全缺省和半缺省参数:
- 全缺省参数:指的是函数的所有参数都设定了默认值。
- 半缺省参数:部分参数设定了默认值,而其余参数没有默认值。C++规定半缺省参数必须从右向左依次连续设定默认值,不能跳过中间参数设置默认值。
// 全缺省 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; }
-
参数传递规则:
- 在使用带有默认参数的函数时,C++要求必须按照参数顺序从左到右依次传递实参,不能跳过中间参数。
-
函数声明和定义的规定:
- 在函数的声明和定义分离时,C++要求默认参数只能在函数声明中指定,而不能在函数定义中重新指定。这意味着,函数的声明应该包含默认参数的设定,而函数的定义中则不应再重复这些默认参数的设定。
这种机制使得函数调用更加灵活和简洁,允许开发者在需要时省略一些常用的参数,同时保持函数接口的一致性。默认参数的使用不仅简化了函数的调用,还有助于提高代码的可读性和可维护性。
void Fun(int a = 0)
{
cout << a << endl;
}
int main()
{
Fun();// 没有传参时,使⽤参数的默认值
Fun(66);// 传参时,使⽤指定的实参
return 0;
}
十、函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样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;
}
十一、引用
引用是C++中一种别名机制,允许我们使用现有变量的别名来访问和操作它。与指针不同,引用在创建时必须初始化,并且一旦指向了一个对象,就不能再指向其他对象,这使得引用更安全、更易于使用。
类型& 引⽤别名=引⽤对象
#include<iostream>
using namespace std;
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
运行结果
通过结果可以知道a,b,c,d地址相同
引⽤的特性
引⽤在定义时必须初始化
⼀个变量可以有多个引⽤
引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
C++引⽤不能改变指向
引用的使用
引用在C++中的应用主要集中在两个方面:引用传参和引用作为返回值,它们都能够减少拷贝的开销,提高效率,并且能够直接修改引用对象的值。
引用传参
引用传参类似于指针传参,但更为方便和直观。通过引用传参,函数可以直接访问和修改传入参数的值,而无需对参数进行复制。
void increment(int &num) {
num++;
}
int main() {
int x = 10;
increment(x);
std::cout << x << std::endl; // 输出 11
return 0;
}
在这个例子中,increment
函数通过引用传参直接修改了x
的值,使其增加了1。
引用作为返回值
引用也可以作为函数的返回值,这样可以直接操作函数内部定义的变量或计算结果,而无需通过复制进行返回。
int& getLarger(int &a, int &b) {
return (a > b) ? a : b;
}
int main() {
int x = 10, y = 20;
getLarger(x, y) = 50; // 将较大的数赋值为50
std::cout << x << ", " << y << std::endl; // 输出 10, 50
return 0;
}
在这个例子中,getLarger
函数返回了两个整数中较大的那个数的引用,使得我们可以直接对其进行赋值操作。
引用与指针的比较
虽然引用和指针在某些功能上有重叠,但它们各有特点,不可互相替代。主要区别在于:
- 引用必须在定义时初始化,并且不能更改指向其他对象,这使得引用更为安全和直观。
- 指针可以在运行时改变指向,可以为空(nullptr)。
C++引用与其他语言的引用的区别
C++中的引用与其他语言如Java的引用有显著区别。在C++中,一旦引用定义后,就不能再改变其指向的对象;而在Java中,引用可以随时指向不同的对象。这一特性使得C++的引用在语义和用法上有更多限制和保证。
结论
在C++中,引用是一种强大的工具,能够提高代码的可读性和性能。引用传参和引用返回值尤其适合用于需要直接操作和修改数据的场合,避免了复制带来的额外开销和潜在的数据不一致性。了解引用的基本原理和使用方法,对于编写高效、清晰的C++代码至关重要。
十二、const引⽤
在C++中,const
引用用于引用一个常量对象或使对象在引用期间不可修改。使用const
引用可以避免意外修改数据,并且在需要传递大型对象时提高效率。
引用常量对象
你可以引用一个const
对象,但必须使用const
引用。const
引用也可以引用普通对象,因为引用的访问权限可以缩小,但不能扩大。
const int &refConst = 10; // 引用一个常量
int a = 20;
const int &refA = a; // 引用一个普通对象
引用临时对象
当你尝试引用一个表达式的结果或类型转换的结果时,C++会创建一个临时对象来存储这个结果。这些临时对象具有常性,因此你必须使用const
引用来引用它们。
int a = 3;
const int &rb = a * 3; // 引用一个表达式的结果
double d = 12.34;
const int &rd = d; // 引用一个类型转换的结果
在上述例子中,a * 3
和d
都会生成临时对象来存储结果,这些临时对象是常量,所以我们需要使用const
引用来引用它们。
临时对象
int a = 3;
const int &rb = a * 3; // 引用一个表达式的结果
double d = 12.34;
const int &rd = d; // 引用一个类型转换的结果
所谓临时对象,是指编译器在计算表达式时,为了暂存求值结果而临时创建的未命名对象。C++规定这些临时对象具有常性,因此只能用const
引用来引用它们。
const
引用是C++中一个重要的概念,它不仅可以引用常量对象,还可以引用普通对象和临时对象。在表达式计算和类型转换中,临时对象的使用尤为频繁。通过了解和正确使用const
引用,可以有效避免不必要的拷贝操作,保证代码的效率和安全性。
十三、指针和引⽤的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功 能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。 • 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
十四、inline
• ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联 函数就需要建⽴栈帧了,就可以提⾼效率。
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展 开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁 调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数。
• vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下 以下两个地⽅。
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地 址,链接时会出现报错。
十五、nullptr
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在C++中,nullptr是一个特殊的关键字,用于表示空指针常量。相比于早期使用的NULL,nullptr解决了许多由于NULL的多义性而引起的问题。
问题来源
在早期的C++标准中,NULL可能被定义为字面常量0,或者类似于C中的无类型指针(void*)的常量。这种定义导致了一些问题:
- 如果将NULL用于函数调用时,可能会调用到错误的函数,因为编译器可能会将NULL解释为整数0,而不是指针。
- 如果强制类型转换使用(void*)NULL,则会导致类型转换问题,因为NULL被定义为0,这不是一个合法的指针类型。
nullptr的引入
为了解决这些问题,C++11引入了nullptr,它是一个特殊的字面量,专门用来表示空指针。nullptr可以隐式地转换为任何指针类型,但不能转换为整数类型。
void f(int* ptr) {
std::cout << "Calling f(int*)" << std::endl;
}
void f(void* ptr) {
std::cout << "Calling f(void*)" << std::endl;
}
int main() {
f(NULL); // 可能调用 f(int*)
f((void*)NULL); // 可能会报错或调用错误的函数
f(nullptr); // 调用 f(int*),nullptr隐式转换为int*
return 0;
}
在上述示例中:
- 使用NULL时,可能会调用到错误的函数,因为NULL被定义为0,可能被解释为整数而非指针。
- 使用(void*)NULL进行类型转换可能会导致类型不匹配的问题。
- 使用nullptr时,它会隐式转换为合适的指针类型,保证了调用的正确性。
nullptr作为C++11中的新特性,解决了使用NULL可能引起的多义性和类型转换问题。它是一种安全和明确表示空指针的方式,在现代C++编程中推荐使用nullptr来代替NULL,以避免潜在的编程错误和不一致性。