C++ 基础

………………………………………………………………………………

命名空间:

C语言环境中,当变量或函数发生冲突的时候,编译器会报错。在一些大工程中不同的板块往往由不同的人员进行编写,合并到一块时可能会出现名字冲突,因此C++在C的基础上引入了命名空间的概念,生成出一块块独立的作用域来避免冲突。

局部变量和全局变量:
为了更好地讲述命名空间,先来复习一下局部变量和全局变量

int a=0;
void test(){
	int a=1;
	printf("%d",a);
}
int main(){
	test();
}

运行上面的代码,输出结果时1
原因是因为a=0属于全局域,而a=1属于局部域,在调用的时候编译器会先选择去局部域寻找目标,再去全局域寻找(局部优先原则

那么如果我不想要输出1,想要直接去打印全局域中0,要怎么做呢?
C++中提供了一种操作符号 ::,用来指定被访问的作用域,左值为作用域,右值为变量名。当左值为空是,默认从全局中访问。

int a=0;
void test(){
	int a=1;
	printf("%d",::a); //输出0
}
int main(){
	test();
}

namespace关键字:
C++中提供namespace关键字方便程序员自定义作用域,如图所示

namespace N
{
	int a=0;
	int b=0;
}    //不用加分号
namespace M
{
	int a=0;
	int b=0;
}

定义了一个名为N的作用域和一个名为M的作用,它们分别放有各自的局部变量a,b,虽然变量名相同,但由于作用域不同,并不会发生冲突

不仅仅是可以指定变量的作用域,也可以指定函数的作用域,此外命名空间是可以嵌套使用的,如图所示↓

namespace N
{
	double add(int a, int b) {
		return a + b;
	}
}
namespace M
{
	float add(int a, int b) {x,y
		return a + b;
	}
	namespace K
	{
		int add(int a, int b) {
			return a + b;
		}
	}
}

使用作用域:
C++提供三种作用域的使用方式

  1. ::操作符,以上面的代码为例子,如果我们要使用作用域N中的add函数,就需要输入N::add(x,y),如果要访问K作用域中的add函数,就需要输入M::K::add(x,y)
  2. using关键字全部展开,人为地把局部作用域中的变量和函数放到全局中去,比如说要把N作用域中的内容放到全局中,输入using namespace N;即可
  3. using关键字部分展开,using M::add,即把M中的add展开到全局,但K不做处理

一般在平时练习中习惯使用第二种方式,在实际工作中杜绝使用此方式,常使用第一、三种方式,如果全部展开过多可能又会出现冲突。

补充:C++标准库中的函数或者是对象都是在命名空间std中定义的 ,当我们要使用标准函数库中的函数或对象都要使用std限定

C++输入输出:

C语言中我们进行标准输出的时候一般都是使用printf函数,但printf函数在使用时必须使用占位符,显得较为麻烦
C++命名空间std中给出了cout对象和cin对象,即标准输出控制台和标准输入台,在调用时需要引入头文件【C++头文件引入时不需要加.h

#include 
int main()
{
	int a=0;
	cin>>a; // 键盘输入
	cout<<a<<endl;  //屏幕输出
}

这里的<<、>>操作不是左移和右移,而是代表流入,endl表示换行,我们这里暂且记住,大致了解,具体原因还需要等到理解类和对象后才能领悟。

cout输出的优势在于它能够自动识别类型,省去了输入占位符的操作

缺省参数:

回顾一下我们我们C中的顺序表结构的创建

struct SeqList
{
	int*a;
	//......
};
void Init(struct SeqList* obj)
{
	obj->a=(int*)malloc(sizeof(int)*4);
	//......
}

void Init(struct SeqList* obj,int n=4)//C++环境下的改进版
{
	obj->a=(int*)malloc(sizeof(int)*n);
	//......
	//不传入n就开4个空间,传入n就开n个空间
}

在顺序表初始化时之前是规定一开始开辟四个空间的,但在实际使用中初始化后往往可能需要多次扩容,这样会影响效率,因此C++中使用了缺省参数来弥补这个不足
所谓缺省参数,就在在函数声明或定义的时候给定形参默认值,倘若调用函数时未对缺省值传参,则使用默认值,反之使用传入值。

全缺省参数:
定义或声明函数时全部的形参都给定默认值

void test(int a=1,int b=2,int c=3);

在调用时可以传入0——3个参数,但是必须从左到右依次传参,编译器是不支持只给第1、3个形参传值,跳过第2个形参的

//调用
test();
test(1);
test(1,2);
test(1,2,3);//√

test(,1,2);test(1, ,3);//这两个是非法操作

.
半缺省参数:
.从右到左依次缺省参数

void test(int a,int b=0,int c=1);
//void test(int a,int b,int c=0);

补充:

  1. 缺省参数默认值必须是常量或全局量
  2. 声明和定义只需要有一个设置默认值就可以,不能够同时设置默认值

函数重载:

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

void Add(int a,int b);
void Add(double a,int b);
void Add(int a,double b);
void Add(int a,int b,int c);

上面四个函数在C++环境下是可以在一个作用域中同时存在的,但在C环境下不行
这里需要牵扯到一些汇编知识,我们进行大致了解
编译的时候C不会对函数进行任何修饰,而C++会根据函数名字长度和形参类型进行修饰,从而实现不同目标下的调用。

C:C++ 基础_第1张图片
C++:C++ 基础_第2张图片
extern “C”:
可以强制编译器对extern "C"修饰的函数按照C的方式进行编译

int add(int a,int b);
double add(int a,int b);
int add(int a,int b=1);

上述情形是不构成重载的,因为它们都不满足构成重载的要求

引用:

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

使用方式:类型& 引用变量名(对象名) = 引用实体

int a=0;
int& b=a;

简单来说,a就是b,b就是a,对a or b的一切操作都是等价的,例如b++会改变a的值,b=x就是把x赋值给a(不是把b作为x的引用)

引用作参数:
引用在一些场景下会使得代码变得更加简洁,更易懂
例如我们常常使用的交换函数Swap

//普通写法
void Swap(int* a,int* b);
Swap(&a,&b); //调用

//使用引用
void Swap(int& a,int& b);
Swap(a,b); //调用

显然可以看出引用作为参数可以使函数被调用是更为方便
在C阶段学习链表操作的时候,很多人常常被二级指针搞得晕头转向,如今进入C++环境后,我们一般很少使用二级指针,通常用一级指针引用代替(例如int*&)

引用作返回值:

int fun1()
{
	int n=0;n++;
	return n;
}
int fun2()
{
	static int n=0;n++;
	return n;
}

上述代码,当我们调用fun1时是直接返回n值吗?
实则不然,因为fun1中的n作为一个局部变量,它的生命在return完后就已经结束了,准确来说通过调用fun1得到的值是n的一个拷贝,这个拷贝通常存放在寄存器或main栈帧中,这就意味着系统需要开辟新空间给予拷贝。
那么我们看fun2,由于n被static修饰,因此它存储在静态空间中,return结束后依然存在,那么理论上来说我们是不需要额外开辟空间拷贝的,但是编译器依然会拷贝传值,因此对于这种情况拷贝是多余的操作,我们在C++环境下可以进行优化

int& fun2()
{
	static int n=0;n++;
	return n;
}

这样子就是告诉编译器我要返回一个n的引用,即就是返回n这个变量本身,这样就避免了白白开辟空间的过程,一定程度上提高了运行效率

注意点:引用能够作为返回值的要求是非常严格的,只有当函数对象是一个生命周期大于当前栈帧的生命周期是才可以使用引用返回,否则可能会出现未定义的结果
举个例子:
C++ 基础_第3张图片
调用fun函数时在fun栈帧中创建了z变量,返回值就是z,但是return结束后fun所在栈帧被销毁,但是ret的地址保持不变,在每一次打印的时候ret所在的栈帧在不断更新的过程中,可能被清理,出现一个随机值的结果。

指针和引用不同点:

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

指针和引用相同点:
不允许权限放大,只允许权限相等或缩小

const a=0;
int* p2=a;
const int a=0;
int& ra=a;

——两段代码编译是不通过的,原因因为a在声明的时候已经说明a为变量,不容许后续改变,而在创建指针or引用是却规定a是可读可写的,这样就非法了。

内联函数:

回顾一下C语言中的宏缺点:

1. 无法调试
2. 安全性低
3. 复杂情况结果难以预料

以上种种缺点衍生出了C++中的内联函数,即宏的升级版,以inline修饰的函数称之为内联函数,
在调用内联函数的时候不需要进行栈帧创建,而是直接展开执行,这样就提高了运行速度,但也是一种以空间换时间的方式,因此代码很长或者有循环、递归的函数不适宜使用作为内联函数

inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联

inline不要声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到(涉及到一些有关汇编的知识,这里不做深入探讨)
(内联函数声明和定义分离在不同文件中的话,定义函数的文件不会将函数和函数地址放在符号表中,声明函数的文件在使用该函数时会找不到该函数,所以会发生里链接错误)

auto关键字:

int main()
{
 int a = 10;
 auto b = a;
 auto c = 'a';

 cout << typeid(b).name() << endl;
 cout << typeid(c).name() << endl;
 cout << typeid(a).name() << endl;//typeid函数可以输出变量类型
 
 return 0;
}

auto修饰的变量可以自动识别右值的类型并对其进行拷贝,上述代码只是作为演示功能,实际运用中对于int、char这类简洁的类型关键字没有必要使用auto拷贝,auto的主要功能在于简化名字较长的自定义变量类型亦或是类中的变量类型,这个学习到后期就可以感受得到。

auto关键字易错点:

1. auto修饰的变量必须进行初始化
2. auto修饰的变量不能传入数组类型
3. auto修饰的变量不能作为形参
4. auto b=a,c=d;类似于这种操作时必须保证a,d的类型相同(编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量)

for语法糖:

C语言中对数组进行打印时往往通过for(int i=0;i C++对其进行了优化
通过for(auto x:arr)的方式迭代打印
本质上就是将arr中的每一个元素一一赋值给x变量,自动识别结束
是不是显得更智能更简便了呢?
(for循环迭代的范围必须是确定的)

nullptr:

之前在C语言环境中常常使用的NULL其实是一个宏,C中将NULL定义为0,因此在使用空指针的时候可能为出现意想不到的结果

void f(int)
{
 cout<<"f(int)"<<endl;
}
void f(int*)
{
 cout<<"f(int*)"<<endl;
}
int main()
{
 f(0);
 f(NULL);
 return 0;
}

理想情况下应该是输出int 和 int*,但是结果是两个int,这偏离了正确的值,原因就在于NULL这里被定义成了整型0
C++中的nullptr就相当于对NULL打了一个补丁,使得f(nullptr)能成功返回预想结果。因此我们在C++使用过程中基本上都会用nullptr代替NULL。

……………………………………………………………………

……
……
……
本文到此结束,如果你觉得可以的话,能否给我来个三连捏,蟹蟹!
至此!

你可能感兴趣的:(c++,算法,c语言,经验分享,笔记)