【C++】C++入门(上)

前言

C++在C语言的基础上扩展而来,兼容C语言,解决了C语言的一些弊端与缺陷,并且容纳了面向对象编程思想,拥有许多强大实用的库,以及编程范式

今天我们先来了解学习C++是如何对C语言的一些设计缺陷进行优化的

命名空间


命名空间旨在解决命名冲突的问题,包括程序员与库、程序员与程序员之间的命名冲突

程序员与库的命名冲突是指:程序员创建的变量或者函数与库中的变量或者函数发生命名冲突
而程序员与程序员的命名冲突就是两个程序员的命名发生冲突,通常发生在项目合作中

首先来看下面一段代码

#include <stdio.h>

int rand = 1;
int main()
{
	printf("%d", rand);
	return 0;
}

rand是全局变量,在局部函数中输出rand,没有问题,下面对代码稍作修改,包含头文件stdlib.h

#include <stdio.h>
#include <stdlib.h>

int rand = 1;
int main()
{
	printf("%d", rand);
	return 0;
}

运行发生了报错:

image-20240117212324853

rand发生重定义,我们知道 ,程序预处理阶段头文件将会展开,而头文件 stdlib.h 中包含函数 rand(),那么 rand() 就与全局变量 rand 发生命名冲突,导致重定义错误

在解决上面的问题之前,我们先来看一些前置知识

编译器在进行搜索时,会遵循以下原则:

  1. 搜索局部域
  2. 搜索全局域
  3. 指定一个域,如果指定了,优先去搜索指定域,然后是局部,全局

接着看以下代码:

#include <stdio.h>

int x = 1;
int main()
{
	int x = 0;
	printf("%d\n", x);
	return 0;
}

一个全局变量 x,一个局部变量 x,在打印时会优先搜索局部域,打印结果为 0

那如果我们想打印全局变量 x 呢?那就需要域作用限定符 ::

::的左边接作用域,如果空着什么都不写的话,默认是全局域。::的右边接作用域的变量或者函数等

如果想打印 全局变量x就接域作用限定符就可以了,::x

#include <stdio.h>

int x = 1;
int main()
{
	int x = 0;
	printf("局部变量x:%d\n", x);
	printf("全局变量x:%d\n", ::x);
	return 0;
}

image-20240117214257622

下面我们来看命名空间 namespace

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}
中即为命名空间的成员。成员可以是变量、函数和结构体等

命名空间限定了一个域,命名空间中的成员只作用于该命名空间

既然命名空间是一个域,那么命名空间也就可以配合域作用限定符使用

直接上代码

#include <stdio.h>

namespace ns1
{
	int x = 1;
}

namespace ns2
{
	int x = 2;
}
int main()
{
	printf("ns1-x:%d\n", ns1::x);
	printf("ns2-x:%d\n", ns2::x);
	return 0;
}

命名空间 ns1 中定义了变量 x,命名空间 ns2 中也定义了变量 x,两个变量 x 虽然同名,但并不会发生冲突,因为它们的作用域是自己的命名空间

image-20240123164401399

那如果命名空间重名了会怎样呢?编译器会将两个命名空间进行合并,如果两个重名的命名空间中有重名变量,那这两个变量就会冲突

命名空间重名,变量重名:

#include <stdio.h>

namespace ns1
{
	int x = 1;
}

namespace ns1
{
	int x = 2;
}
int main()
{
	printf("ns1-x:%d\n", ns1::x);
	printf("ns2-x:%d\n", ns1::x);
	return 0;
}

发生冲突:
image-20240117220257594

发生以上情况,要么给命名空间改名,不进行合并;要么将命名空间中的变量改名,防止命名冲突

了解到以上知识后,我们就可以解决最初的问题

#include <stdio.h>
#include <stdlib.h>

int rand = 1;
int main()
{
	printf("%d", rand);
	return 0;
}

image-20240117212324853

只需将变量rand放入命名空间中,在打印时使用域作用限定符

#include <stdio.h>
#include <stdlib.h>

namespace ns1
{
	int rand = 1;
}
int main()
{
	printf("%d\n", ns1::rand);
	printf("%p\n", rand);    // rand为函数名,是一个地址
	return 0;
}

image-20240117220626477

命名空间内成员的使用

现有命名空间如下:

namespace N
{
	int a = 10;
	char b = 'b';
}

想使用此命名空间中的成员有三种方式:

  1. 加命名空间和域作用限定符

    namespace N
    {
    	int a = 10;
    	char b = 'b';
    }
    
    int main()
    {
    	printf("%d\n", N::a);
    	printf("%c\n", N::b);
    	return 0;
    }
    

    这样可以精确访问命名空间内的成员,但是如果要大量使用某个成员,都要加命名空间和域作用限定符,就会有点繁琐

  2. 使用 using将命名空间内的某个成员引入

    #include <stdio.h>
    
    namespace N
    {
    	int a = 10;
    	char b = 'b';
    }
    
    using N::a;	// 将命名空间中的 a 引入
    
    int main()
    {
    	printf("%d\n", a);	// 这里使用 using 引入了a,直接就能使用
    	printf("%c\n", N::b);	// b没有使用 using 引用,所以使用时需要引用
    	return 0;
    }
    
  3. 使用 using namespace将整个命名空间引入

#include <stdio.h>

namespace N
{
	int a = 10;
	char b = 'b';
}

using namespace N;	// 将整个命名空间 N 引入

int main()
{
	printf("%d\n", a);	// 命名空间已经引入,其中的成员都可以直接使用
	printf("%c\n", b);
	return 0;
}

这里不要把引入命名空间和包含头文件搞混了。
文件中包含的头文件会在预处理阶段展开,就是将头文件代码拷贝到文件中
而引入命名空间是让编译器到指定命名空间进行搜索;不引入命名空间,编译器只会搜索局部域和全局域,不会去搜索特定命名空间

当我们使用 using namespace 命名空间展开时,如果代码较多,容易发生冲突的问题,因此这种方式只推荐在日常练习代码时使用,在编写大型项目时,建议使用前两种引用方式

C++的输入和输出


C++兼容C语言的输入输出方式,也有自己特有的输入输出

首先要引入头文件和命名空间

#include <iostream>
using namespace std;

std 是C++标准库的命名空间,里面存放着标准库的定义的实现

C++的头文件不带.h,一些旧的编译器(vc6.0)还支持 <iostream.h>

输出

使用 cout 标准输出对象(控制台)和流插入运算符 <<来进行输出

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello World!" << endl;	// endl是特殊C++符号,表示换行输出
	return 0;
}

可以形象地记忆为 将要输出的内容顺着箭头插入到控制台

image-20240121184703068

输入

使用 cin标准输入对象(键盘)和流提取运算符 >>进行输入

#include <iostream>
using namespace std;

int main()
{
	int a = 0;

	cin >> a ;
	cout << "a:" << a << endl;

	return 0;
}

也可以像上面那样记忆,将键盘的内容顺着箭头放到目的地

image-20240121184806358

不难看出,使用coutcin,与使用printf不同,不用我们指定格式,它们会自动识别变量类型

std命名空间的使用

std标准库的内容很多,日常使用时直接展开即可

如果进行项目编写时,不建议这样做,容易发生冲突

建议 std::cin 或者using std::cin

#include <iostream>
using std::cin;	// 只引入cin
int main()
{
	int a = 0;

	cin >> a ;
	std::cout << "a:" << a << std::endl;	// 引入 count 和 endl

	return 0;
}

缺省参数


缺省参数是在声明或定义函数时为函数的参数指定一个缺省值。在调用这个函数时,如果没有指定实参,那么形参就是指定的缺省值;否则使用指定的实参

void Func(int a = 0)	// 缺省值为0
{
	cout << a << endl;
}

int main()
{
	Func();	// 不传实参,形参为缺省值
	Func(10);	// 传实参,形参为实参的拷贝
}

image-20240121213712818

缺省参数的分类


全缺省参数

全缺省参数是指函数的参数全部缺省,调用函数时只要在函数参数限定个数之内,就可以传任意个参数
比如函数有3个参数,全部是缺省参数,那么调用此函数时可以传0、1、2、3个参数

void Func(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

int main()
{
	Func(1, 2, 3);
	Func(1, 2);
	Func(1);
	Func();
}

image-20240121214705273

半缺省参数

半缺省参数不是说函数的参数有一半是缺省,一半不是
半缺省参数是指函数的参数有一部分是缺省,一部分不是

void Func(int a, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

int main()
{
	Func(1, 2, 3);
	Func(1, 2);
	Func(1);
}

image-20240121215217531

需要注意的是,半缺省参数必须从右向左给出,而且必须连续,不能间隔给

如果从左向右给出缺省

void Func(int a = 10, int b = 20, int c);

像上面这种,如果全部传实参还好,但是如果传的参数不足3个,最后一个参数(非缺省参数)就会少参数

间隔给缺省

void Func(int a, int b = 20, int c, int d = 40)

缺省参数不支持间隔跳着给参数,以上这种情况,必须得给 c 传参,那么就必须给a、b传参,但是这样 b 就是去作为半缺省的意义了

缺省参数的应用

当某个函数的某个参数不确定需要什么值,而我们又需要给出一个默认值时,就需要用到缺省参数

比如说顺序栈的初始化

typedef struct Stack
{
	int* data;
	int size;
	int capacity;
}Stack;

void StackInit(Stack* ps, int capacity = 4)
{
	ps->data = (int*)malloc(sizeof(int) * capacity);
	ps->capacity = capacity;
}

当我们明确知道栈要存多少数据时,就可以给capacity传参,如果不知道要存多少数据,就给缺省值

注意事项

  1. 半缺省参数必须从右往左给出,且必须连续

  2. 如果函数声明与定义分开了,那么缺省参数要放在函数声明中,缺省不能同时存在与声明与定义中

    关于这点,我们来详细看一下

    函数声明与定义分离,一般是模块化编程时才发生

    下面模拟编写栈时的情境

    Stack.h 这里存放的是函数声明

    #include <iostream>
    using namespace std;
    
    typedef struct Stack
    {
    	int* data;
    	int size;
    	int capacity;
    }Stack;
    
    void StackInit(Stack* ps, int capacity = 4);	// 缺省参数放在声明中
    

    Stack.cpp 这里存放的是函数定义

    #include "Stack.h"
    
    void StackInit(Stack* ps, int capacity)		// 没有缺省
    {
    	ps->data = (int*)malloc(sizeof(int) * capacity);
    	ps->capacity = capacity;
    }
    

    test.cpp 这里用于测试函数

    #include "Stack.h"
    
    int main()
    {
    	Stack st;
    	StackInit(&st);
    	return 0;
    }
    

    我们先来验证缺省是否能同时存在于函数声明与定义中

    // Stack.h  函数声明
    void StackInit(Stack* ps, int capacity = 4);	// 包含缺省
    
    // Stack.cpp  函数定义
    void StackInit(Stack* ps, int capacity = 4)		// 包含缺省
    {
    	ps->data = (int*)malloc(sizeof(int) * capacity);
    	ps->capacity = capacity;
    }
    
    // test.cpp 测试
    int main()
    {
    	Stack st;
    	StackInit(&st);
    	return 0;
    }
    

    运行程序,发生报错

    image-20240122110910259

    这时我们再去掉声明中的缺省,保留定义中的缺省

    // Stack.h  函数声明
    void StackInit(Stack* ps, int capacity);	// 去掉缺省
    
    // Stack.cpp  函数定义
    void StackInit(Stack* ps, int capacity = 4)		// 保留缺省
    {
    	ps->data = (int*)malloc(sizeof(int) * capacity);
    	ps->capacity = capacity;
    }
    
    // test.cpp 测试
    int main()
    {
    	Stack st;
    	StackInit(&st);
    	return 0;
    }
    

    试运行,依然报错

    image-20240122111354616

    可以看到,错误发生在 test.cpp 中,虽然在函数定义中有缺省,但在函数声明中是没有缺省的,所以发生了如上错误

    这次我们使用正确方式运行,缺省放在函数声明中,函数定义中不放缺省

    // Stack.h  函数声明
    void StackInit(Stack* ps, int capacity = 4);	// 缺省
    
    // Stack.cpp  函数定义
    void StackInit(Stack* ps, int capacity)		// 不缺省
    {
    	ps->data = (int*)malloc(sizeof(int) * capacity);
    	ps->capacity = capacity;
    }
    
    // test.cpp 测试
    int main()
    {
    	Stack st;
    	StackInit(&st);
    	return 0;
    }
    

    运行正常,没有报错

    image-20240122112020611

    那为什么缺省参数要放在函数声明中呢?

    首先我们要知道,在程序的预处理阶段,会发生头文件展开、宏替换、条件编译、去注释等操作

    在上面的代码中,test.cpp 包含了头文件 Stack.h

    // Stack.h
    #include <iostream>
    using namespace std;
    
    typedef struct Stack
    {
    	int* data;
    	int size;
    	int capacity;
    }Stack;
    
    void StackInit(Stack* ps, int capacity = 4);	// 缺省参数放在声明中
    
    // test.cpp
    #include "Stack.h"
    int main()
    {
    	Stack st;
    	StackInit(&st);
    	return 0;
    }
    

    在预处理阶段,头文件展开,就是将头文件的内容拷贝到 test.cpp 文件中

    // test.cpp
    // ... 拷贝头文件的内容,上面的代码我就省略了,只展示函数声明
    void StackInit(Stack* ps, int capacity = 4);	// 缺省参数放在声明中
    
    int main()
    {
    	Stack st;
    	StackInit(&st);
    	return 0;
    }
    

    预处理结束后,生成一个后缀为 .i 的新文件,内容类似上面的代码,之后会进入编译阶段。在编译阶段,会检查语法是否正确。

    如上文件中有函数的声明,函数声明中含有缺省参数,所以在调用时只传一个参数不存在语法错误

    如果在调用函数时使用缺省参数,而函数声明中没有缺省参数,定义中有缺省参数,在编译阶段会检测出语法错误,导致程序报错

  3. 缺省参数必须是常量或者全局变量

  4. C语言不支持(编译器不支持)

粗略地了解一下程序的翻译链接过程


过程:预处理、编译、汇编、链接

还是模拟编写栈的过程

Stack.h

#include <iostream>
using namespace std;

typedef struct Stack
{
	int* data;
	int size;
	int capacity;
}Stack;

void StackInit(Stack* ps, int capacity = 4);

Stack.cpp

#include "Stack.h"

void StackInit(Stack* ps, int capacity)
{
	ps->data = (int*)malloc(sizeof(int) * capacity);
	ps->capacity = capacity;
}

test.cpp

#include "Stack.h"

int main()
{
	Stack st;
	StackInit(&st);
	return 0;
}

预处理阶段要做的事情:展开头文件、宏替换、条件编译、去掉注释,最终生成的文件以.i结尾

image-20240122160238605

编译阶段:检查语法是否正确、生成汇编代码,生成的文件以.s结尾

image-20240122163920592

在编译阶段,代码转为汇编代码,此时我们就可以了解到函数调用的本质形式:call 函数地址

函数地址又是什么意思呢?这一点和数组有点像

数组由一堆元素组成,数组的地址是首元素的地址

函数由一堆指令组成,函数地址就是函数第一条指令的地址

调用函数实质就是跳转到函数的地址,依次向下执行函数中的指令

在vs中进入调试模式,再转到反汇编:

image-20240122161053896

进入反汇编,我们就可以看到函数调用的汇编形式了,call 函数地址

image-20240122162200314

image-20240122162736299

image-20240122162806615

当文件中没有函数定义,只有函数声明时,暂时是找不到函数的地址的,要到链接阶段寻找函数地址

也就是说,Stack.cpp中有函数的地址,test.cpp中没有函数的地址,函数调用暂时记为call StackInit(?)

汇编阶段,将汇编代码转为二进制机器码,生成的文件以.o结尾

image-20240122163941861

链接阶段:因为只有函数声明的文件找不到函数地址,所以在此阶段,就要凭借函数名去到其他文件中寻找函数地址。最后将各个文件合并到一起,形成可执行程序

image-20240122164445005

函数重载


函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。

我个人理解,函数重载相当于一词多义,一个函数可以有不同用法

函数重载要求:函数名相同,参数不同,即可构成函数重载

  1. 参数类型不同

    // 整数相加
    int Add(int a, int b)
    {
    	cout << "int Add(int a, int b)" << endl;
    	return a + b;
    }
    
    // 浮点数相加
    double Add(double a, double b)
    {
    	cout << "double Add(double a, double b)" << endl;
    	return a + b;
    }
    
    int main()
    {
    	Add(1, 2);
    	Add(1.1, 2.2);
    }
    

    image-20240122172615651

  2. 参数数量不同

    // 零参数
    void f()
    {
    	cout << "f()" << endl;
    }
    // 1参数
    void f(int a)
    {
    	cout << "f(int a)" << endl;
    }
    int main()
    {
    	f();
    	f(1);
    }
    

    image-20240122172915504

  3. 参数顺序不同(本质还是类型不同)

//参数顺序不同
void func(int a, char b)
{
	cout << "func(int a, char b)" << endl;
}

void func(char a, int b)
{
	cout << "func(char a, int b)" << endl;
}

image-20240122173904976

C++函数重载原理

为什么C语言不支持函数重载,而C++支持呢?

首先我们要知道,函数重载虽然函数名相同,但是函数地址不同

int Add(int a, int b);
double Add(double a, double b);
// 以上两函数的地址不同

image-20240123170124486

image-20240123170153499

之前我们已经了解过,在程序的翻译链接阶段,C语言是通过函数名来寻找函数地址的:call 函数名(函数地址),C语言无法区分相同的函数名,那就找不到相应的函数地址,自然不支持函数重载

而C++支持函数重载,要寻找函数地址,就要区分相同的函数名。那C++是如何区分的呢?——名字修饰(name Mangling)

具体怎么修饰,不同编译器的修饰规则不同。下面我们以Linux下的g++为例,了解一下修饰规则是怎么样的。为什么不用Windowsvs?相对来说太复杂了,不如 g++通俗易懂

g++名字修饰规则:_Z+函数名长度+函数名+参数类型首字母

int Add(int a, int b);	// call _Z3Addii
double Add(double a, double b);	// call _Z3Adddd

下面我们就到Linux下进行验证

C语言编译器编译结果

现有test.c文件,内容如下

image-20240122181029641

image-20240122181133140

编译test.c文件,形成可执行文件testc

gcc -o testc test.c

image-20240122181323445

反编译testc

objdump -S testc

image-20240122181714483

可以看到,在Linux下,使用gcc完成编译后,函数名字的修饰没有发生改变

C++编译器编译结果

现有C++文件test.cc,文件内容不变

image-20240122182023078

image-20240122182040678

使用g++编译test.cc,得到testcpp

g++ -o testcpp test.cc

image-20240122182251481

接着进行反编译

objdump -S testcpp

image-20240122182356023

可以看到,在Linux下使用g++完成编译后,函数名字的修饰发生改变,编译器将函数的参数类型信息添加到修改后的名字中

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值