C++初阶02:关键字+命名空间+输入&输出+缺省参数+函数重载+extern C+引用+内联函数

23 篇文章 1 订阅
19 篇文章 0 订阅

1. C++关键字

学习过一定编程的人,对于关键字来说肯定已经不算陌生了,例如for while static break等,当敲进编译器中颜色发生改变了,就说明它是作为关键字来定义的, 那么进入到C++阶段,关键字在C基础上有了更多的延申,(C++支持C的语法,但C不支持C++)下面就是C++会用到的关键字的大概汇总,当然也是只供参考,真正向都掌握这些关键字也还要在接下来的学习中掌握。

asmdoifreturn trycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

2. 命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

那么,该如何定义一个命名空间呢?

//一、
//命名冲突问题
//1.我们自己定义的变量、函数名称可能跟库里的重名
//2.进入公司项目组以后,做的项目通常比较大,多人协作,写的代码中命名冲突
//C语言没办法很好解决这个问题,只能换个名字
//C++(CPP)提出的新语法:命名空间

//定义了一个叫bit的命名空间 -- 域(全局)
namespace bit
{
	//里面定义的变量也是全局变量,放到静态区
	int rand = 0;
	int Add(int x, int y)
	{
		return x + y;
	}
	struct Node
	{
		struct Node* next;
		int val;
	};
}
void func()
{
	//局部域
}
//命名空间可以嵌套
namespace N1
{
	int x = 0;
	int y = 0;
	int Add(int x, int y)
	{
		return x + y + 10;
	}
	namespace N2
	{
		int a = 0;
		int b = 0;
		int Sub(int a, int b)
		{
			return a - b;
		}
	}
	struct Node
	{
		struct Node* next;
		int val;
	};
}
//那么,既然命名空间是为了避免命名冲突,那如果需要了,如何访问到自己定义
//的命名空间中的变量呢?
// 介绍一个新符号 :: 作用域访问符
namespace bit
{
	//里面定义的变量也是全局变量,放到静态区
	int rand = 0;
	int Add(int x, int y)
	{
		return x + y;
	}
	struct Node
	{
		struct Node* next;
		int val;
	};
}
int a = 0;
int main()
{
	//先局部后全局,不会进入自定义域内找
	printf("hello world\n");
	printf("%d\n", rand);
	printf("%d\n", bit::rand);// :: 作用域访问符
    //这样就可以访问到bit命名空间中rand变量了
	int a = 1;
	printf("%d\n", a);
	printf("%d\n", ::a);//在全局域中找
	return 0;
}

当一个大项目需要很多人同时工作时候,每个人负责的部分有限,难免会出面命名冲突的情况,那么这时候就可以自己封装一个命名空间,等需要了,就可以展开一下自己的命名空间,在没有使用的情况下可以对自己命名空间内的变量和函数等起到保护作用,那该如何展开呢?

//把整个命名空间展开 -- 展开到全局了,虽然用起来方便了,但是隔离失效了
//using namespace bit;
//单独展开某一个,其他不展开,用于展开常用的东西
namespace bit
{
	//里面定义的变量也是全局变量,放到静态区
	int rand = 0;
	int Add(int x, int y)
	{
		return x + y;
	}
	struct Node
	{
		struct Node* next;
		int val;
	};
}
//再介绍一个新关键字:using 可以展开我们制定的命名空间或者命名空间内的成员供我们使用
using bit::Node;
int main()
{
	bit::rand = 10;

	struct bit::Node node;
	printf("%d\n", bit::Add(1, 2));
	printf("%d\n", N1::Add(1, 2));

	printf("%d\n", N1::N2::Sub(2, 1));

	return 0;
}

这样呢,bit命名空间内的Node结构体就可以被当作全局变量被使用了, 当然也可以直接展开整个bit,但是这样的话就相当于bit失去了本身命名空间的保护,所以当用到哪个成员就展开哪个成员。

3. C++输入&输出

printf("hello world\n");  相信这句代码是所有程序员或者学习编程的人最先了解的一句代码,打印"hello world"这个字符串在屏幕上,printf是C语言的写法,那到了C++必定会有改动,无论是输入和输出都有C++的重新标准,于是就引出了输入流和输出流,那如何用C++在屏幕上打印"hello world"呢?

#include <iostream> //stdio.h
using namespace std; //C++库的实现定义在一个叫std的命名空间中
int main()
{
    //一个新的关键字:cout 输出
    // << 流插入运算符
	cout << "helloworld" << endl;
	return 0;
}

//不展开std命名空间:
#include <iostream> 
//using namespace std; 
int main()
{
	std::cout << "helloworld" << std::endl;
	return 0;
}

和printf需要的<stdio,h>一样,C++的输入流也需要一个<iostream>的头文件,io意为in&out,stream意为流,所以这也就是输入和输出流都需要的头文件,而下面的using namespace std;是使用了C++自己定义的命名空间,名为std。

cout << "hello world" << endl;不难发现,为何起名为流,字符串和endl向水流一般流入了cout,输出的时候也是输出cout,cout里就包括了hello world字符串和endl。

那输入流呢?

#include <iostream>
using namespace std;
int mian()
{
    //介绍一个新关键字:cin 输入
    // >> 流提取运算符
    //自动识别类型
	cin >> i >> d;
	cout << i << " " << d << endl;
    return 0;
}

 和cout相反,cin使用的是 >> 好像在接纳i和d,并且在使用cin输入的时候是自动识别类型的,不必再像C语言一样还要写上%d%c,这也是进步的一方面,当然如果还是想用printf写也可以,毕竟C++是兼容C语言的所有语法的。

4. 缺省参数

不少情侣其实都有“缺省参数”的行为,在热恋中人们都是无脑的,但是一旦闹了矛盾就动了向分开的心思,但习惯了两人生活的自己又怎么能很快地适应单人生活呢?于是在还未分手的时候就为自己物色好了分手后的对象,要学习的C++中的缺省参数就像上文提到的这样,当在一个没有形参的接口中使用了某些未定义的参数,就默认使用自动为其指定的参数,参数的值也是默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

//缺省参数
void Func(int a = 0)
{
	cout << a << endl;
}
int main()
{
    TestFunc(); // 没有传参时,使用参数的默认值
    TestFunc(10); // 传参时,使用指定的实参
}

在第一次调用时并没有给TestFunc传参,但是在封装这个接口的时候规定了如果没传参就要使用int a = 0,这就是缺省参数,这样调用下去a就会以0来执行;但当接口穿了参数,参数就会以穿的参数为标准来执行,a的值就是10。

C++中缺省值还分这几种情况:

//1.缺省参数
void Func(int a = 0)
{
	cout << a << endl;
}

//2.全缺省:所有的参数都给了缺省值
void Func(int a = 10, int b = 20, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl << endl;
}

//3.半缺省:缺省部分参数(必须从右向左缺省)
//void Func(int a, int b = 20, int c = 30)
void Func(int a, int b, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl << endl;
}

int main()
{
	//如果不传参数,a就会拿缺省值去做实参,就是0
	//Func();
	//Func(1); 
	//Func(1, 2);
	//Func(1, 2, 3);

	//Func(1); 
	//Func(1, 2);
	//Func(1, 2, 3);

	return 0;
}

注意:

1. 半缺省参数必须从右往左依次来给出,不能间隔着给

2. 缺省参数不能在函数声明和定义中同时出现

void TestFunc(int a = 10);

void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
//个缺省值。

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

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

5. 函数重载

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

通俗地讲:重载就是为了使函数更加灵活方便,即使参数有细微差别但也可以运行,语法仍然支持。

参数类型不同

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;
}

名同为Add,但调用时却根据实参的类型而定,如果传进的实参是double类型,就调用第二个,如果是int型就调用第一个。

参数个数不同

void f()
{
	cout << "void f()" << endl;
}
void f(int a)
{
	cout << "void f(int a)" << endl;
}

如果f传参了就调用二个,没有传参就调用第一个,还是依据实参而定。

传参的顺序不同

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

注意:①返回值不同不能构成重载,因为调用的时候不能区分

int f(double d)
{
	...
}
double f(double d)
{
	...
}
//不构成重载

只有符合上面的三种情况才构成重载,重载和返回值并没有关系。

注意:②缺省值不同不构成重载

void f(int a)
{
	cout << "void f(int a)" << endl;

}
void f(int b = 0)
{
	cout << "void f(int b = 0)" << endl;
}
int main()
{
	//f();
	//f(1);
	return 0;
}

②构成重载,但是使用时会有问题

void f()
{
	cout << "void f(int a)" << endl;

}
void f(int b = 0)
{
	cout << "void f(int b = 0)" << endl;
}
int main()
{
	//f(); //不传参时调用存在歧义
	//f(1);

	return 0;
}

f()调用时根本分不清是第一个在调用还是第二个在调用。

那么为什么C++支持重载呢?这要追溯到文件的编译链接了,在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。

1. 实际我们的项目通常是由多个头文件和多个源文件构成,而通过我们C语言阶段学习的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有 Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?

2. 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符 号表中找Add的地址,然后链接到一起。(如果同学们忘记了上面过程,咋们老师要带同学们回顾一下)

3. 那么链接时,面对Add函数,连接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。

4. 由于Windows下vs的修饰规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字。

5. 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类 型首字母】。

采用C语言编译器编译后结果:

结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。 

采用C++编译器编译后结果:

结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。

Windows下名字修饰规则:

对比Linux会发现,windows下C++编译器对函数名字修饰非常诡异,但道理都是一样的。

6. 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。

7. 另外我们也理解了,为什么函数重载要求参数不同,而跟返回值没关系。

6.extern C

有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器, 将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree 两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。

并且学过了函数的重载,知道程序在为函数命名的时候规则有不同,extern C的作用就是让编译器分清楚哪些是C的程序,需要用C的命名规则来命名,哪些是C++的。

//告诉C++编译器,{}里面的函数是C编译器编译的,链接的时候用C的函数名修饰规则去找才能链接上
extern "C"
{
#include "../../2021C++classcode/2021.11.30DS/Stack.h"//..可以访问下一级文件
}

在数据结构阶段使用C语言实现过Stack,那么如果C++某个工程或者项目需要用到C中的东西,就可以用extern "C"来包一下该文件的头文件,这样Stack.h就会以C的形式在cpp文件中展开,就可以使用其中的接口或变量了。

C也可以调用C++的库,需要在C的头文件中

#ifdef _cplusplus
	#define EXTERN_C extern "C"
#else
	#define EXTERN_C
#endif
EXTERN_C
{
	void StackInit(ST* ps);
	void StackDestroy(ST* ps);
	void StackPush(ST* ps, STDataType x);
	void StackPop(ST* ps);
	STDataType StackTop(ST* ps);
	int StackSize(ST* ps);
	bool StackEmpty(ST* ps);
}
//或者
#ifdef _cplusplus
	#define EXTERN_C extern "C"
#else
	#define EXTERN_C
#endif
EXTERN_C void StackInit(ST* ps);
EXTERN_C void StackDestroy(ST* ps);
EXTERN_C void StackPush(ST* ps, STDataType x);
EXTERN_C void StackPop(ST* ps);
EXTERN_C STDataType StackTop(ST* ps);
EXTERN_C int StackSize(ST* ps);
EXTERN_C bool StackEmpty(ST* ps);

总结:
1.C++调用C的库,在C++程序中加extern "C"
2.C调用C++的库,在C++库中加extern "C"

7. 引用

到这里就会发现C语言和C++既像又不像的地方了,学过C语言都知道指针,定义一个指针可以指向一块空间,并且通过这个指针可以访问到这块空间,可是一定要和接下来这个只是分清楚:

引用 - 并没有开辟新空间,只是对原来的空间取了一个新名称

int main()
{
    int a = 10;
    int& b = a; //&b - 取地址
    
    a = 20;
    b = 30; //无论改a或改b,改的都是同一块空间
    return 0;
}

那么int& 就是引用,这相当于b和a现在公用一块空间,一块空间叫两个名字,无论通过哪个名称访问都会访问到一块空间,这就是引用,相当于起了个外号一般,和指针还是挺不像的对吧。

一些引用的注意事项:

int main()
{
	//1.引用必须再定时时初始化
	//int a = 10;
	//int& b; //err
	
	//2.一个变量可以有多个引用
	//int a = 10;
	//int& b = a;
	//int& c = a;
	//int& d = b;

	//double d = 1.1;

	//3.引用一旦引用一个实体,再不能引用其他实体
	//int a = 10;
	//int& b = a;

	//int c = 20;
	(1)这里是让b变成c的别名 ×
	(2)把c赋值给b √
	//b = c;

	//int* p = &a;
	//p = &c;

	return 0;
}

引用做参数时:

void Swap(int a, int b)//传值
{
	int tmp = a;
	a = b;
	b = tmp;
}
void Swap(int* p1, int* p2) //传地址
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void Swap(int& r1, int& r2) //传引用
{
	int tmp = r1;
	r1 = r2;
	r2 = tmp;
}
//它们三个构成重载,但是调用时不明确,存在歧义,不知道调用传值还是传引用,所以会报错
int main()
{

	int x = 0;
	int y = 1;
	Swap(&x, &y);
	Swap(x, y);
	Swap(x, y);

	return 0;
}

引用做输出型参数:

int* sigleNumbers(int* nums, int numSize, int& returnSize) - 引用
int* sigleNumbers(int* nums, int numSize, int* returnSize)
{
	int* a = (int*)malloc(sizeof(int) * 2);
	//...

	*returnSize = 2;
	return a;
}

引用做返回值:

//临时变量存在哪呢?
//1.如果c比较小(4 or 8),一般是寄存器充当临时变量
//2.如果c比较大,临时变量放在调用Add函数的栈帧中
int Add(int a, int b)
{
	int c = a + b;
	return c;
}
int& Add(int a, int b) //err
{
	int c = a + b;
	return c;
	//不生成c的拷贝,直接返回c的引用
	//c -> int& tmp
	//return tmp;
}
//当前代码的问题:
//1.存在非法访问,因为Add(1,2)的返回值是c的引用,所以Add栈帧销毁了以后,回去访问c位置的空间
//2.如果Add函数栈帧销毁,清理空间,那么c的值取到的就是随机值,给ret的就是随机值,取决于当前编译器
//ps:vs下销毁栈帧没有请空间数据
int& Count()
{
	static int n = 0;//如果出了作用域返回对象还在(没有还给操作系统),则可以使用引用返回,如果已经还了,那就不能用引用返回
	n++;
	return n;
}

总结:引用的作用主要体现在传值、传参、传返回值
1.引用传参、传返回值,在有些场景下面可以提高性能(大对象+深拷贝对象)
2.引用传参、传返回值,输出型参数和输出型返回值。通俗点说,有些场景下面形参的改变可以改变实参。有些场景下面,引用返回可以改变返回对象。

常引用:

int main()
{
	//权限放大 ×
	//const int a = 10; //const只读
	//int& b = a;
	 
	//权限不变 √
	const int a = 10;
	const int& b = a;
	 
	//权限缩小 √
	int c = 10;
	const int& d = c;

	f(c);
	f(a); //不加const没有权限
	f(10);

	return 0;
}

常引用主要起到的作用是对权限的扩大和缩小,可以有效的保护变量只被访问不被改变。

那既然引用和指针在某些功能上很相似,那么引用和指针有什么区别呢?

指针和引用的区别:
1. 引用在定义时必须初始化,指针没有要求。
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
3. 没有NULL引用,但有NULL指针。
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。(32位平台下占4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
6. 有多级指针,但是没有多级引用。
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
8. 引用比指针使用起来相对更安全。

8. 内联函数

在调用函数时是会有栈帧的创建的,栈帧中还保存着一些寄存器,那么它们改如何恢复呢?这些都是有消耗的,面对频繁调用的小函数,能否优化呢?

int Add(int x, int y)
{
    int ret = x + y;
    return ret;
}

int main()
{


    Add(1, 2);
    Add(1, 2);
    Add(1, 2);
    Add(1, 2);
    Add(1, 2);
    Add(1, 2);


    return 0;
}
//C -- 宏(替换)
//#define Add(x, y) ((x)+(y))
inline int Add(int x, int y)
{
    int ret = x + y;
    return ret;
}
//有了内联,就不需要C的宏,因为宏比较复杂
int main()
{
    cout << Add(1, 2) << endl;

    return 0;
}

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

内联的特点:

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

结论:短小的,频繁调用的函数建议用内联。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值