【C++】入门基础

前言

C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。本篇大致介绍C++如何填补C语言留下的坑,以及C++是如何对C语言设计不合理的地方进行优化的,为后续类和对象学习打下基础。

C++是向下兼容C语言的,所以C++文件也可以使用C语言代码。

一、命名空间

C语言中的命名冲突

那么如果我们定义的变量名字与库函数中的变量或者函数名一样起冲突了怎么办?

在这里插入图片描述

abs()stdlib.h文件中定义的函数,如果我们定义到全局域,就会与库函数中的变量或者函数名冲突。C语言没办法解决类似这样的命名冲突问题。

那不可以放到局部域吗?当然可以。

在这里插入图片描述

因为遵循就近原则,所以程序会优先搜索局部域,找不到才会去搜索全局域(C语言的知识),所以写到局部域就不会报错,但是C++有更好的解决方案

实际应用中,一个项目往往是多人合作完成的,很有可能会出现多个人写的函数名或者变量名一样,使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字
污染,namespace关键字的出现就是针对这种问题的。

命名空间的定义

namespace关键字后面加上命名空间的名字(一般开发中是用项目名字做命名空间名),然后加一对{}{}中即为命名空间的成员。

命名空间中可以定义变量/函数/类型。

在这里插入图片描述

但是命名空间只能在全局域定义,不能在函数内部定义!

命名空间可以嵌套使用

在这里插入图片描述

同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

因为本质是一个命名空间,所以不能存在相同变量/函数/类型,也就是相同命名空间内部不能出现命名冲突。

在这里插入图片描述

命名空间的使用

C语言中只有两个作用域:局部域和全局域。
C++中有四个作用域:局部域、全局域、命名空间域、类域。

域作用限定符:两个冒号:: 后面访问命名空间域和类域时会经常用到。

之前在C语言中,这个符号也是用来解决命名冲突的问题。

int a = 100;

int main()
{
	int a = 11;
	printf("%d\n", a);//打印11
	printf("%d\n", ::a);//打印100
	return 0;
}

程序会优先搜索局部域,再搜索全局域,如果局部域和全局域变量名一样,那么访问局部域就需要用到域作用限定符::

以下命名空间为例来访问其中的变量或函数。

namespace ball
{
	int x = 10;
	int y = 20;
	int Add(int a, int b)
	{
		return a + b;
	}
}

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

int main()
{
	printf("%d\n", ball::x);
	printf("%d\n", ball::y);

	int a = 1, b = 2;
	printf("%d\n", ball::Add(a, b));

	return 0;
}

2.使用关键字using将命名空间中某个成员引入

using ball::x;
using ball::y;
using ball::Add;

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

	int a = 1, b = 2;
	printf("%d\n", Add(a, b));
	return 0;
}

3.使用using namespace命名空间名称引入

using namespace ball;

int main()
{
	printf("%d\n", x);
	printf("%d\n", y);
	
	int a = 1, b = 2;
	printf("%d\n", Add(a, b));
	return 0;
}

但是,展开命名空间域后,全局定义和命名空间域的定义会不明确。

在这里插入图片描述

还有一种特殊的命名空间:无名的命名空间

在这里插入图片描述
可以不通过域作用限定符(也无法通过)直接访问命名空间中的成员。具有内部链接性,不能在命名空间所属文件以外的地方使用该命名空间里面的成员。

std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中.
也就是我们常见的

using namespace std;

我们日常练习的时候推荐这样直接展开std,但是以后写项目的时候不建议直接将标准命名空间展开,否则自己定义的东西容易跟库里的起冲突,推荐用下面两种写法,比较安全。

using std::cin;
using std::cout;
using std::endl;
int main()
{
	int a;
	cin >> a;
	cout << a << std::endl;
	return 0;
}

也可以边使用边展开。

int main()
{
	int a;
	std::cin >> a;
	std::cout << a << std::endl;
	return 0;
}

总结

1.不同命名空间域可以有同名变量(本质是两个变量),同命名空间域中不能有同名变量,可以用嵌套命名空间域解决。
2. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中,但是不可有同名变量。
3.程序会优先搜索局部域,其次全局域,默认不会搜索命名空间域,需要用上述三种方法来访问命名空间中的成员。
4.命名空间只能在全局域定义,不能在函数内部定义。

二、C++输入与输出

流插入运算符:<<
流提取运算符:>>
cin:标准输入对象
cout:标准输出对象

cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<iostream>头文件中,使用时按命名空间的使用方法使用std。

#include <iostream>
using namespace std;//或者using std::cout;
int main()
{
	cout << "aaabbcc" << endl;
	return 0; 
}
//或者
#include <iostream>
int main()
{
	std::cout << "aaabbcc" << endl;
	return 0;
}

C语言的printf、scanf需要手动控制格式。C++的输入输出可以自动识别变量类型,使用更方便。

在这里插入图片描述

但是并不是任何情况下都方便使用,比如控制浮点数输出精度,控制整形输出进制格式等,使用的并不多,因为这些使用C语言实现更简单。

例如下面C++控制浮点数输出精度的方法

在这里插入图片描述cout默认保留6位有效数字,并且会自动删去小数点结尾的0,使用cout控制输入小数点位数需要按照上述方法,并且包含头文件iomanip,使用起来麻烦,远没有scanf简单。

printf("%.8lf\n", x);

tip:并且在一些算法题中,数据量比较大的情况下,scanf和printf要比cin和cout的效率高,使用cin和cout可能会超时。

总而言之,哪个方便用哪个,也可以混合使用,C++是向下兼容C语言的,可以使用C语言的的代码。

三、缺省参数

C语言是不支持缺省参数的。

缺省参数概念:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

在这里插入图片描述

全缺省参数:全部参数都有缺省值

在这里插入图片描述

半缺省参数:只有部分参数有默认值

在这里插入图片描述

注意:
1.半缺省参数必须从右往左依次来给,不能间隔着给,这是因为函数参数时是从左往右依次传参的

错误写法:

void Func(int a = 10, int b, int c = 30)
{
	...
}
void Func(int a = 10, int b, int c)
{
	...
}

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

因为如果缺省参数在声明与定义同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。所以规定:在声明时给缺省值,定义时不给。

在这里插入图片描述

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

在这里插入图片描述

四、函数重载

函数重载:在C++中,同一个作用域下,多个函数的函数名字相同,形参列表不同,称为函数重载,常用来处理功能类似而数据类型不同的问题。

1.参数类型不同构成函数重载

在这里插入图片描述

2.参数个数不同构成参数重载

在这里插入图片描述

3.参数类型顺序不同构成重载

在这里插入图片描述

注意:返回值类型不同不会构成函数重载

以下两个函数构成重载,但是调用会出现问题,无参调用存在歧义。

void Func()
{
	cout << "Func()" << endl;
}
void Func(int x = 0)
{
	cout << "Func(int x)" << endl;
}

C语言为什么不能实现函数重载?
底层原因是C语言和C++在编译链接阶段对函数名字的修饰规则不同。
C语言是直接通过函数名去查找,重载函数的函数名是相同的,所以存在歧义。
而C++对函数名字的修饰规则与参数有关,参数不同,修饰出来的名字就不一样,因此支持函数重载。

总结

1.函数重载必须满足参数列表不同:参数个数不同/参数类型不同/类型顺序不同。
2.返回值类型不同不构成函数重载,因为C++对函数名的修饰规则与返回值无关。

五、引用

引用是给变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块空间,所以一个发生改变,两个都会改变。

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

在这里插入图片描述

引用特性

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

int a = 1;
//int& ra;	错误写法
int& ra = a;

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

int a = 1;
int& b = a;
int& c = a;

3.一个引用变量只能在初始化时引用一个实体,后面不能再引用其他实体。

在这里插入图片描述可以看到,执行b = x之后,a,b,c的值都发生了改变。这是因为a,b,c公用一块空间,将x赋值给b,也就改变了这块空间的内容,相当于同时赋值给a和c。

4.不能引用常量

int& ra = 10;//错误

常引用

在这里插入图片描述
引用变量类型和实体类型不同,要使用常引用。

在这里插入图片描述

使用场景

1.引用做参数
既不是传值也不是传地址,传引用直接可以实现两个数交换。

int Swap(int& x, int& y)
{
	int temp = x;
	x = y;
	y = x;
}

2.引用做返回值

值返回会产生一个临时变量,返回结果赋给这个临时变量,再将这个临时变量赋给接收的变量ret。由于临时变量具有常性,所以要用常引用接收。

在这里插入图片描述
但是上述代码还有问题。与下面的代码放到一起说明。

引用返回不会产生临时变量返回,直接返回结果的引用也就是别名。

在这里插入图片描述

但是,上面两段代码都存在潜在问题ret的打印结果是不确定的。

Add函数结束,栈帧会销毁,如果没有清理栈帧,那么ret的结果侥幸是正确的;如果清理了栈帧,那么返回结果z的空间会被系统回收,ret的结果就会是随机值。

如果用引用接收函数的返回结果,再次调用函数,ret的结果也会发生改变。调用完函数完,栈帧销毁,再次调用该函数会使用同一块空间,所以结果就会被覆盖

在这里插入图片描述

函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

因此我们可以用static来修饰返回结果,延长其生命周期至main()结束。

int& Add(int x, int y)
{
	static int z = x + y;
	return z;
}

或者返回全局变量。

int n;//全局变量
int& Add(int x, int y)
{
	n = x + y;
	return n;
}

引用做返回值可以修改返回值

int n = 0;//全局变量
int& Func(int x)
{
	n = 10 * x;
	return n;
}
int main()
{
	Func(1) = 11;
	cout << n << endl;//11
	return 0;
}

传值、传引用效率比较:以值作为参数或返回值类型,会先生成临时拷贝再返回,因此效率是非常低的,尤其是当参数或返回值类型非常大时。

总结

1.引用传参的使用场景非常广泛,基本任何场景都可以用引用传参。
2.谨慎用引用做返回值。出了函数作用域,对象不存在就不能用引用返回,还在就可以用引用返回。
3.引用做返回值可以修改返回值。
4.引用返回可以减少拷贝提高效率。

引用与指针的区别

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

六、内联函数

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

常规函数会调用call指令来调用函数。

在这里插入图片描述

内联函数直接在使用的地方展开函数体,没有函数调用的开销。

在这里插入图片描述

内联函数特性

1.内联函数是拿空间换取时间提高程序运行效率的,没有函数调用建立栈帧的开销。代码过长不适合用inline(否则目标文件会很大),内联函数适用于短小频繁调用的函数

2.内联函数对于编译器来说只是一个建议最终是否成为内联函数,取决于编译器。不同编译器关于inline的实现机制可能不同。一般代码较长的函数或者递归函数,即使加了inline也被会否决掉。(vs默认debug模式下inline不会起作用,需要手动设置或切换release模式)

3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

七、auto关键字

C++11规定:auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

auto可以根据表达式右边的类型自动推导左边的类型。

int a = 0;
auto b = a;//int b = a;

int* p = &a;
auto q = p;//int* q = &a;

auto ch = 'a';//char ch = 'a';

使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型

auto x;//错误!

auto使用规则:

1.用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int x = 0;
auto p1 = &x;
auto* p2 = &x;
auto& rx = x;

2.当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错。

auto a = 1, b = 2;
auto c = 3, d = 3.14;//报错

3.auto不能做函数的参数

void Test(auto x)//错误,编译器无法推导x的实际类型
{}

4.auto不能直接声明数组

在这里插入图片描述

5.for循环与auto的结合使用

for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

int arr[] = { 1,2,3,4,5 };
for (auto& x : arr)//改变数组中的内容要使用引用
{
	x++;
}
for (auto x : arr)
{
	cout << x << ' ';
}

以下代码就有问题,因为for的范围不确定。

void Print(int arr[])
{
	for (auto& e : arr)
		cout << e << endl;
}

八、指针空值nullptr

之前C语言中学的NULL实际是一个宏,定义在传统的C头文件(stddef.h)中

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

可以看到,NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:

在这里插入图片描述
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。

C++11中新引入关键字nullptr来解决这一问题。使用时无需包含头文件。

在这里插入图片描述

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值