C++的发展史

学习C++语言,我们首先来认识一个人,他就是C++之父——本贾尼.斯特劳斯特卢普
在这里插入图片描述

C++ 由本贾尼・斯特劳斯特卢普于 20 世纪 80 年代在 C 语言基础上创立,最初名为 “C with Classes”,核心是为 C 语言增添面向对象特性(类、继承等);1989 年正式定名 C++,1998 年推出首个国际标准 C++98 奠定基础;后续历经 C++03(小幅修订)、C++11(重大更新,引入 Lambda、智能指针等现代特性)、C++14/17/20/23 等版本迭代,逐步完善泛型编程、并发编程支持,优化性能与安全性,始终保持对 C 语言的兼容性,成为兼顾底层控制与高层抽象的通用编程语言。

1.C++版本的更新
在这里插入图片描述

  • ps:我这边给大家推荐两个网站链接有助于C++的学习:
    https://legacy.cplusplus.com/reference/(更新至C++11)
    https://en.cppreference.com/w/(C++官方文档)

命名空间

1.命名空间的概念

命名空间是新定义的一个作用域,里面可以放函数,变量和定义类等,主要是用于防止命名的冲突
符号“::”在C++中叫做作用域限定符,我们通过“命名空间名称::命名空间成员”便可以访问到命名空间中相应的成员。

2.命名空间的实现
(1)普通定义:namespace(关键字)+命名空间名字{命名空间成员}

namespace zkh
{
  int a=0;//定义变量
  int Add(int x,int y)//定义函数
  {
   int ret=x+y;
   return ret;
  }
}

(2)嵌套定义

namespace zkh
{
	//fzd
	namespace fzd
	{
		int rand = 1;

		int Add(int left, int right)
		{
			return left + right;
		}
	}

	//zjk
	namespace zjk
	{
		int rand = 2;
		
		int Add(int left, int right)
		{
			return (left + right) * 10;
		}
	}
}


int main()
{
	printf("%d\n", zkh::fzd::rand);
	printf("%d\n", zkh::zjk::rand);

	printf("%d\n", zkh::fzd::Add(1, 2));
	printf("%d\n", zkh::zjk::Add(1, 2));
}

(3)同一工程中允许存在相同名称的命名空间,编译器最后会将所有命名空间合并成一个

  • Queue.h 含namespace zkh命名空间
  • Queue.cpp含namespace zkh命名空间
  • test.cpp含namespace zkh命名空间
    以上是队列实现的头文件,函数实现文件,测试文件均有相同命名空间的定义,但是最后都会被合并到一起

3.命名空间的成员使用

#include<stdio.h>

namespace N
{
  int x=2;
  int y=3;
}

//1.N::成员名
int main()
{
  printf("%d",N::a);
  return 0;
}

//2.using N::成员名
using N::y;
int main()
{
  printf("%d\n", N::x);//x使用时需要加上前缀N::
	printf("%d\n", y);//y可以直接使用
	return 0;
}

//3.展开命名空间全部成员:项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤
using namespace std;

int main()
{
  printf("%d",x);
  printf("%d",y);
}

总结一下:在 C/C++ 中,变量的访问遵循 “就近原则”(也叫 “作用域屏蔽规则”),核心优先顺序为:局部变量 > 全局变量,如果我们想要直接访问全局的变量需要再变量前面加上::域限定符

C++的输入和输出

还记得我们刚开始学C语言的时候,写了一个最简单的“hello world”字符打印在屏幕上。下面我们就用同样的方式用C++的形式去打印一下这个字符串。

#include<stdio.h>
//c语言
int main()
{
  printf("hello world\n";
  return 0;
}
//C++
#include<iostream>
using namespace std;

int main()
{
     cout<<"hello world"<<endl;
     return 0;
}

我们可以对比一下上面的两段代码,可以发现头文件改变了,而原来的printf和scanf被cin和cout所替换,接下来我们介绍一下C++的cin和cout
1.cin和cout标准输入输出

< iostream >是 InputOutputStream的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象
std::cin是istream类的对象,它主要⾯向窄字符(narrowcharacters(oftypechar))的标准输入流
std::cout是ostream类的对象,它主要⾯向窄字符的标准输出流。
std::endl是⼀个函数,流插入输出时,相当于插入⼀个换行字符加刷新缓冲区

使用C++输⼊输出更方便,不需要像printf/scanf输⼊输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输⼊输出。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
int main()
{
 int a = 0;
 double b = 0.1;
 char c = 'x';
 cout << a << " " << b << " " << c << endl;
 std::cout << a << " " << b << " " << c << std::endl;
 scanf("%d%lf", &a, &b);
 printf("%d %lf\n", a, b);
 // 可以⾃动识别变量的类型 
 cin >> a;
 cin >> b >> c;
 cout << a << endl;
 cout << b << " " << c << endl;
 return 0;
}

接下来说说C++与C语言的兼容,我们知道C++是C语言的基础上发展起来的,所以C++不仅兼容C语言的头文件,而且兼容printf和scanf。正常使用过程中我们可以将两者混合去使用,比如我们需要限定小数点的位数,输入的格式符,这时候用printf和scanf就会明显比cin和cout更方便,并且C++和C语言的缓冲区保持同步

//同步刷新保证两者输出的顺序是一致的,否则就会导致乱序
    printf("C: Hello ");  // C的stdio输出
    cout << "C++: World!" << endl;  // C++的iostream输出
//关闭同步刷新,提高效率
#include<iostream>
using namespace std;
int main()
{
 // 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码 
 // 可以提⾼C++IO效率 
 ios_base::sync_with_stdio(false);//关闭C语言同步
 cin.tie(nullptr);
 cout.tie(nullptr);
 return 0;
}

缺省参数

缺省参数的定义

缺省参数是声明或者定义从参数时为函数的参数指定一个默认值,在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定实参

缺省参数的分类

半缺省参数

顾名思义就是部分函数带有缺省值,但是缺省值必须从右往左依次给出;

void Print(int a,int b=2)
{
  cout<<a<<b<<endl;
}

全缺省参数

顾名思义就是全部形参给缺省值

void Print(int a=1,int b=2)
{
 cout<<a<<b<<endl;
}
int main()
{
 Print(); // 没有传参时,使⽤参数的默认值 
 Print(5,6); // 传参时,使⽤指定的实参 
 return 0;
}

注意事项

(1)半缺省参数只能从右往左依次给出,不能跳跃给参;

void Print(int a=2,int b)//错误,不能跳跃给参
{
  cout<<a<<b<<endl;
}

(2)函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值

//同时给出导致编译器不知道该用哪一个
void Print(int a=2int b=3);//声明
void Print(int a=3int b=4//定义
{
  cout<<a<<b<<endl;
}

(3)缺省参数必须是常用或者全局变量,既在编译时要能够明确知道缺省值的内容
(4)C语言不支持缺省函数

函数重载

概念和调用原理

函数重载是相同作用域,函数名相同,参数列表不同,具体体现在参数类型不同,个数不同以及类型的次序不同,与返回值类型是否相同无关

调用原理:函数名相同,编译器会优先调用哪个函数呢?编译期间,编译器通过对实参类型进行推演,根据推演结果找对应的重载函数,如果存在且不会造成二义性则调用,否则产生编译错误;

#include<iostream>
using namespace std;
// 1、参数类型不同 
int Add(int x, int y)
{
 cout << "int Add(int x, int y)" << endl;
 return x + y;
}
double Add(double x, double y)
{
 cout << "double Add(double x, double y)" << endl;
 return x + y;
}
// 2、参数个数不同 
void print()
{
 cout << "print()" << endl;
}
void print(int a)
{
 cout << "print(int a)" << endl;
}
// 3、参数类型顺序不同 
void print(int a, char b)
{
 cout << "print(int a,char b)" << endl;
}
void f(char b, int a)
{
 cout << "print(char b, int a)" << endl;
}

名字修饰规则(C与C++差异)

1.C 语言的修饰规则极其简单:直接保留函数名(或加下划线_前缀),不包含任何参数信息。

// C语言代码
int add(int a, int b);
double add(double a, double b); // 编译报错:重定义(编译器无法区分)
//修饰后符号(GCC 环境):两个add都会被修饰为_add(仅加下划线前缀);
//链接器会认为是同一个函数,直接报 “重定义错误”,因此 C 语言不支持重载。

2.==C++ ==的修饰规则核心逻辑:前缀 + 函数名长度 + 函数名 + 参数类型编码 + 后缀,不同编译器的编码格式略有差异:
比如gcc编译器:_Z + 函数名长度 + 函数名 + 参数类型缩写

// 1. 无参数函数
void func() { }
// 修饰后:_Z4funcv(_Z + 4(func长度) + func + v(void参数))

// 2. 两个int参数的add
int add(int a, int b) { }
// 修饰后:_Z3addii(_Z + 3(add长度) + add + i(int)+ i(int))

// 3. 两个double参数的add(重载)
double add(double a, double b) { }
// 修饰后:_Z3adddd(_Z + 3(add长度) + add + d(double)+ d(double))

// 4. 带引用参数的函数
void swap(int& x, int& y) { }
// 修饰后:_Z4swapRiRi(_Z + 4(swap长度) + swap + Ri(int&)+ Ri(int&))

// 5. 命名空间内的函数
namespace N1 {
    int sub(int a, int b) { }
}
// 修饰后:_ZN2N13subii(_Z + N(命名空间)+ 2(N1长度)+ N1 + 3(sub长度)+ sub + ii)

结论:重载函数的参数类型 / 个数 / 顺序不同,会导致修饰后的符号名不同(如_Z3addii vs _Z3adddd),链接器能区分,因此支持重载。所以这也就验证了为什么返回值类型不同为什么不能作为函数重载的判断条件;
3.特殊情况:extern “C” 的作用(强制按 C 规则修饰)

  • 如果想让 C++ 函数按 C 语言的规则修饰(比如供 C 代码调用),可以用extern "C"声明,强制编译器禁用 C++ 的名字修饰:
// C++代码
extern "C" { // 块内函数按C规则修饰
    int add(int a, int b) { return a + b; }
}

// 修饰结果(GCC):_add(和C语言一致,而非_Z3addii)

限制:extern “C” 修饰的函数可以跨语言调用,但是不能重载(因为按 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都是指向同一块内存空间就像水浒传里面的黑旋风 -李逵,但是宋江也叫他铁牛,名字有很多,但是指的都是同一个人物;

引用的特性

1.引用在定义的时候必须初始化;

int a=2int& b;//错误
int& b=a;//正确

2.一个变量可以有多个引用

int a=3;
int& b=a;
int& c=a;
int& d=b;

3.引用一旦引用了一个实体再不能引用其他实体

int x=3;
int z=8;
int& y=x;//正确,此时y已经引用了x
int& y=z;//错误,y不能在引用其他实体

const引用

用const修饰的引用,该引用变量不能修改

  • 可以引用⼀个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
#include <iostream>
using namespace std;

int main() {
    // 定义一个const对象(权限:只读)
    const int a = 10;

    // 错误写法:普通引用绑定const对象
    // int& ra = a; 
    // 报错原因:权限放大(原对象是const只读,普通引用是可修改,违反规则)

    // 正确写法:const引用绑定const对象
    const int& ra = a; 
    cout << "const引用访问const对象:" << ra << endl; // 输出10

    return 0;
}
#include <iostream>
using namespace std;

int main() {
    int b = 20;

    // 正确写法:const引用绑定普通对象(权限从“可修改”缩小到“只读”)
    const int& rb = b; 

    // 注意:原对象b本身是普通变量,仍可直接修改
    b = 30;
    // 错误写法:不能通过const引用修改对象
    // rb = 40; 
    // 报错原因:const引用的权限是只读,禁止修改

    return 0;
}
  • 在类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。

所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。权限只读,不可修改。

#include<iostream>
using namespace std;
int main()
{
 int a = 10;
 const int& ra = 30;
 
 // 编译报错: “初始化”: ⽆法从“int”转换为“int &” 
 // int& rb = a * 3;
 const int& rb = a*3;
 double d = 12.34;
 // 编译报错:“初始化”: ⽆法从“double”转换为“int &” 
 // int& rd = d;
 const int& rd = d;
 return 0;
}

引用的三大场景

1.用引用简化嵌套结构体成员的访问

当结构体嵌套层级较多时(比如A.B.C.成员),重复写多层访问符会很繁琐。用引用绑定内层成员,之后直接用 “别名” 访问,能大幅简化代码。

#include <iostream>
#include <string>
using namespace std;

// 嵌套结构体:内层结构体
struct StudentInfo {
    int id;       // 学生学号
    string name;  // 学生姓名
    double score; // 学生成绩
};

// 外层结构体:包含内层结构体
struct ClassInfo {
    string className;    // 班级名称
    StudentInfo topStu;  // 嵌套的“学生信息”结构体
};
int main() {
    // 定义外层结构体对象
    ClassInfo class1 = {
        "高三(1)班",
        {1001, "张三", 95.5} // 内层StudentInfo的初始化
    };
    StudentInfo& stuRef = class1.topStu;
     cout<< "xx班级的第一名是" << stuRef.name  // 直接用stuRef替代class1.topStu
    

2.1做函数参数 —— 通过形参修改外部实参

引用作为函数参数时,形参是实参的 “别名”,修改形参等价于修改实参;对比指针,引用无需解引用、不会出现野指针,更简洁安全。

#include <iostream>
using namespace std;

// 用引用做参数:直接修改外部实参
void swapByRef(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

// 对比:用指针做参数(繁琐且易出错)
void swapByPtr(int* a, int* b) {
    if (a == nullptr || b == nullptr) { // 必须加空指针判断,否则崩溃
        return;
    }
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    
    // 引用传参:写法简洁,无需取地址
    swapByRef(x, y);
    cout << "引用交换后:x=" << x << ", y=" << y << endl; // 输出 x=20, y=10

    // 指针传参:需要手动取地址,且有野指针风险
    swapByPtr(&x, &y);
    cout << "指针交换后:x=" << x << ", y=" << y << endl; // 输出 x=10, y=20

    return 0;
}

2.2自定义类型对象传参 —— 用引用提高效率

自定义类型(结构体、类)传值时会触发 “对象拷贝”(开销大),而引用传参是直接访问原对象,无拷贝、效率高;搭配const还能保证只读安全。

#include <iostream>
#include <string>
using namespace std;

// 自定义类型:学生结构体(包含大字段,拷贝开销高)
struct Student {
    string name; // 字符串是复杂类型,拷贝开销大
    int age;
    double score;
};

// 错误写法:传值(会拷贝整个Student对象,效率低)
void printStudent_Val(Student s) {
    cout << "传值-姓名:" << s.name << ",年龄:" << s.age << endl;
}

// 推荐写法:const引用传参(无拷贝,且保证只读)
void printStudent_Ref(const Student& s) {
    cout << "引用-姓名:" << s.name << ",年龄:" << s.age << endl;
    // s.age = 20; // 编译报错:const引用禁止修改实参,保证安全
}

3.做函数返回值 —— 禁止返回栈空间的引用

函数内的局部变量存储在 “栈空间”,函数结束后栈空间会被回收;若返回局部变量的引用,会变成 “悬空引用”(指向已释放的内存)。

#include <iostream>
using namespace std;
// 正确示例:返回静态/全局变量的引用
int& goodReturnRef() {
    static int num = 200; // static变量存储在静态区,函数结束后不会释放
    return num;
}
int main() {
    int& ref2 = goodReturnRef();
    cout << "正确返回:" << ref2 << endl; // 输出200

    // 可以通过引用修改静态变量(因为其生命周期未结束)
    ref2 = 300;
    cout << "修改后返回:" << goodReturnRef() << endl; // 输出300

    return 0;
}

引用和指针的区别

引用在底层实现上就是按照指针的方式去实现的,即引用在底层就是一个指针,A&–>A* const,const A&–>const A* const

在这里插入图片描述
根据以上反汇编的代码来看,引用与指针的底层其实是相类似的

两者的区别主要体现在特性和使用的形式上;

  • 引用在定义的时候必须要初始化,而指针没有要求;
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型的实体;
  • 没有空引用,但是有NULL指针;
  • 在sizeof含义不同,引用结果为引用类型的大小,但是指针始终是地址空间所占字节个数(32位平台占4个字节)
  • 有多级指针,但是没有多级指针
  • 访问实体的方式不同,指针需要显式解引用,引用则是编译器自己处理
  • 引用比指针使用起来更加的安全
  • 引用变量的++和–是直接给引用实体的值+1或者-1,而指针变量的++和–是让该指针向前或者向后偏移一个类型的大小

内联函数

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率

#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
 int ret = x + y;
 ret += 5;
 ret += 5;
 ret += 5;
 return ret;
}
int main()
{
 // 可以通过汇编观察程序是否展开 
 // 有call Add语句就是没有展开,没有就是展开了 
 int ret = Add(3, 2);
 cout << Add(3, 2) * 5 << endl;
 return 0;
}

在这里插入图片描述

内联函数的特性

1.inline 是一种以空间换时间的做法,省去调用函数栈开销。所以代码很长或者有循环 / 递归的函数不适宜使用作为内联函数
2.inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义内联的函数体内有循环 / 递归等等,编译器优化时会忽略掉内联。
3.inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。

内联和宏函数的区别

在这里插入图片描述

nullptr

C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。

#include<iostream>
using namespace std;
void f(int x)
{
 cout << "f(int x)" << endl;
}
void f(int* ptr)
{
 cout << "f(int* ptr)" << endl;
}
int main()
{
 f(0);
 // 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。 
 f(NULL);
 f((int*)NULL);
 // 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型 
 // f((void*)NULL);
 
 f(nullptr);
 return 0;
}

C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些麻烦*

NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:

#ifndef NULL
 #ifdef __cplusplus
 #define NULL 0
 #else
 #define NULL ((void *)0)
 #endif
#endif

nullptr的核心特性:
C++11 引入nullptr,就是为了解决NULL的歧义,它的核心特性的是:
原生空指针类型:nullptr的类型是std::nullptr_t(C++11 新增的独立类型);
隐式转换安全:std::nullptr_t可以隐式转换为任意指针类型(int*、char*、void*等),但不能隐式转换为int类型;
无歧义:调用重载函数时,nullptr会优先匹配指针类型的参数,而非int类型。

好了相信学到这里,你对C++的基本概念都有了一定的了解,结合前面的C语言部分,你会发现C++的特性和优势所在,并且在后面章节的学习中你会慢慢体会到面向对象的抽象,封装,继承这三大特征;大佬们的一键三连就是对我最好的支持,我们下一章再见!
在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐