C++入门

前言

  在前面学习了 C 语言和数据结构基础之后,终于要开始学习新篇章——C++ 了。我个人觉得前面一段时间的努力是为现在的学习做铺垫,有了之前的基础,我相信在学习 C++ 的道路上会走的更快、更扎实,下面就让我们走进通向 C++ 学习的大门吧。

关键字

  在 c 语言中我们学习了 32 个关键字,而 C++ 在兼容 C 语言的基础上多出了不少关键字,但是我们并不需要将他们背下来,因为在学习的过程中我们会不断的接触到它们,遇到不会的就去查阅资料,慢慢的就可以真正的掌握它们。下面我列出所有的关键字:
在这里插入图片描述

命名空间

  在 C / C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,这样一来可能会导致很多冲突。而使用命名空间的目的则是对标识符的名称进行区域化,以避免命名冲突或名字污染。

namespace

  定义命名空间,需要使用到namespace关键字,语法格式为:

namespace 空间名字 {
	//变量
	//函数
	...
}

  具体的定义格式有三种,展示如下:

//普通
namespace n1 {
	int a = 10;
	int fun(int m, int n){
		return m > n ? m : n;
	}
}
//嵌套
namespace n2 {
	int a = 10;
	int fun(int m, int n) {
		return m > n ? m : n;
	}
	namespace n3 {
		int b = 20;
		int fun(int m, int n) {
			return m > n ? n : m;
		}
	}
}
//分离:同一个项目中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中;
namespace n1 {
	int c = 30;
	int fun1(int m, int n) {
		return m > n ? n : m;
	}
}
使用方式

  在知道如何定义命名空间之后,下面来介绍一下该如何使用命名空间中的成员,首先我们先定义一个命名空间,然后具体的使用方法有三种:

namespace N
{
	int a = 10;
	int b = 20;
	int Add(int left, int right){
		return left + right;
	}
	int Sub(int left, int right){
		return left - right;
	}
}
int main(){
	//printf("%d\n", a); 若直接使用命名空间中的变量,则该语句编译出错,无法识别 a
	return 0;
}
    1. 加命名空间名称及作用域限定符
int main(){
	//命名空间名称::成员
	printf("%d\n", N::a);
	return 0;
}
    1. 使用 using 将命名空间中成员引入
//在使用前将命名空间中的成员拿出,后面使用的时候就可直接使用
using N::b;
int main(){
	printf("%d\n", N::a);
	printf("%d\n", b);
	return 0;
}
    1. 使用 using namespace 将命名空间名称引入
//方法二是将空间中某一成员拿出,而方法三是将空间完全展开,该空间里面的成员都可直接使用,不过不建议大家使用这种方式
using namespce N;
int main(){
	printf("%d\n", N::a);
	printf("%d\n", b);
	Add(10, 20);
	return 0;
}
起别名
  • 有时候命名空间太长,但是又不想展开,我们可以为命名空间起别名;
namespace 新名字=旧命名空间;

输入&输出

  首先 C 语言中我们在输入输出时,需要包含<stdio.h>头文件才可以正常进行,而在 C++ 中则是不同的,我们需要包含另一个头文件——<iostream>,这样才可以进行正常的输入输出。另外要说明的一点是,在 C++ 标准库中所有的功能均在 std 标准命名空间下,所以我们在使用任何 C++ 库中的东西时,都需要前缀std::,这样比较麻烦,此时我么就可以使用上面所说的第三种方法,在头文件下方加上using namespace std;,这样我们就可直接使用标准库中的东西了。
  在 C++ 中的输入输出和 C 语言中的输入输出是有较大差别的,在 C 语言中进行输入输出时,我们需要增加数据格式控制,比如:整形——%d,字符——%c;而在 C++ 中进行输入输出时,我们只需写成如下格式即可:

#include <iostream>
using namespace std;
int main(){
	int a;
	double b;
	char c;
	//输入一个数据给 a
	cin >> a;
	//连续输入,先输入一个数据给 b,再输入一个数据给 c
	cin >> b >> c;
	//输出 a,并换行;c 语言中的换行是'\n',而在 C++ 中则是'endl'
	cout << a << endl;
	//连续输出,输出 b,输出空格,输出 c,并换行
	cout << b << " " << c << endl;
	return 0;
}

  其实输入输出的格式还有很多种,这里我就不一一列举了,因为大部分都用不上,若果有感兴趣的,可以看看这篇博客c++简单格式化输出输入

缺省参数

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

void TestFunc(int a = 0){
	cout << a << endl;
}
int main(){
	TestFunc(); // 没有传参时,使用参数的默认值
	TestFunc(10); // 传参时,使用指定的实参
	return 0;
}
缺省参数分类
  • 全缺省参数:所有参数都有默认值;
void TestFunc(int a = 10, int b = 20, int c = 30){
	cout<<"a = "<<a<<endl;
	cout<<"b = "<<b<<endl;
	cout<<"c = "<<c<<endl;
}
  • 部分缺省参数:部分参数没有默认值,部分参数有默认值;
void TestFunc(int a, int b = 10, int c = 20){
	cout<<"a = "<<a<<endl;
	cout<<"b = "<<b<<endl;
	cout<<"c = "<<c<<endl;
}
注意事项
  1. 半缺省参数必须从右往左依次来给出,不能间隔着给;
void TestFunc(int a, int b = 10, int c = 20);//正确
void TestFunc(int a = 10, int b, int c = 20);//错误
void TestFunc(int a = 10, int b = 20, int c);//错误
  1. 缺省参数不能在函数声明和定义中同时出现;
//下面这个就是错误的,因为在声明和定义中同时给缺省值赋默认值,哪怕默认值一样也不行
int func(int a = 1);
int func(int a = 1) {
	return 0;
}
//在声明和定义时,同一个缺省值只能在一个地方赋默认值

函数重载

概念

  函数重载是函数的一种特殊情况,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表 (参数个数 或 类型 或 顺序) 必须不同,常用来处理实现功能类似数据类型不同的问题,但是要注意的是,C++ 中无法通过返回值类型不同来进行重载;举个例子:

int Add(int left, double right){
	return left+right;
}
//与第一个函数参数个数不同
int Add(int left, double right, double mid){
	return left+right;
}
//与第一个函数参数类型不同
int Add(double left, char right){
	return left+right;
}
//与第一个函数参数顺序不同
int Add(double left, int right){
	return left+right;
}

//这个就会报错,因为返回值类型不同,不能重载
double Add(int left, double right){
	return left+right;
}
重载原理

  为什么 C 语言中不能重载,而在 C++ 中可以重载呢?这是因为在 C 编译器和 C++ 编译器中底层对函数的命名规则不同,也就是说当我们写了一个函数之后,在 C 或 C++ 编译器中对与我们写的这个函数,编译器会有自己的命名规则;下面我来给大家演示一下,由于 vs 的修饰规则过于复杂,而 Linux 下编译器的修饰规则简单易懂,所以我们使用 Linux 下的 gccg++ 来给大家进行演示:

在这里插入图片描述
  我相信通过上面的演示,大家就能理解了 C 语言没办法支持重载,因为同名函数没办法区分。而 C++ 是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载;另外这也解释了为什么在 C++ 中不能通过返回值类型不同来进行重载,因为在命名规则中没有标识返回值的信息,所以不可以。

extern关键字

  有时候在 C++ 项目中可能需要将某些函数按照 C 的风格来编译,那么我们可以在函数前加extern "C"来解决,意思是告诉编译器,这个函数将按照 C 语言规则来编译,举个例子:

extern "C" int Add(int left, int right){
	return left + right;
}
int main(){
	//此函数将按照 C 语言规则来编译
	Add(1,2);
	return 0;
}
//有时我们需要将很多函数来进行 C 语言规则来编译,那么在前面一个一个加 extern "C" 太麻烦了,所以我们可以进行如下操作:
extern "C" {
	//此时在这里面写的函数都将会按照 C 语言规则来编译
}

  extern可以用来声明全局变量extern声明之后就可以扩大全局变量的作用域,本来全局变量的作用域是从定义处开始直到文件结束,使用extern提前声明之后就变成从声明处开始,直到文件结束;另外如果在声明时并赋值,那么就是定义而非声明了;

int main(){
	//声明之后,变量i的作用域从该处开始直到文件结束。
    extern int i;
    //这是定义而非声明
    extern int j = 0;
    cout<<i<<endl;   
    return 0;
}
int i=5;

  一个项目中不同文件想要使用同一全局变量,那么除了包含头文件的方法外,我么也可以使用extern来声明之后使用,但是需要注意,如果是一个const变量,那么在定义的地方也要在前面加上extern才能使用;

file1:
int  i =5;
//const修饰的变量这里必须加上extern才能在别的地方声明之后使用
extern const int j = 1;
file2:
//声明之后就可以使用了
extern  int  i;  
extern const int j;

引用

  什么是引用?通俗一点来说,引用就是给一个变量起别名、起外号,所以不管是用他的本名、别名还是外号来进行操作,最终都是作用在这个变量上的,这就是引用的含义;具体使用起来也很简单,语法格式为:变量类型& 引用变量名(对象名) = 引用实体

void TestRef(){
	int a = 10;
	//ra 引用 a,不管是使用 ra 还是 a 都是在操作同一片空间
	int& ra = a;
	//此时直接修改 ra,最终都会作用在 a 上的
	ra = 20;
	//此时打印出的结果为 20
	cout << a << endl;
}
底层实现和语法层面

底层实现
  引用说白了其实就是被包装过的指针,下面我们来看一段代码:

//用 ra 引用 a,并使用 ra 来修改 a 的值
int a = 10;
int& ra = a;
ra = 100;
//用指针 ra1 指向 a1,并使用 *ra1 来修改 a1 的值
int a1 = 10;
int* ra1 = &a1;
*ra1 = 100;

  上面这一小段代码在运行之后,我们重点来看它的底层汇编实现,如下图所示,我们来进行横向对比,就会发现,在底层其实引用的指令和指针的指令一模一样,我们可以得出结论,在底层实现上,引用的本质就是指针,只不过系统内部自动执行了解引用的操作,我们在使用的时候可以省略掉这一步骤;
在这里插入图片描述
语法层面
  首先我们来看下面这一端代码,他最终得到的结果是:相同;此时我相信很多小伙伴就有疑问了,上面明明说的是,引用的本质就是指针,所以 a 有自己的空间,ra 也有自己的空间,只不过 ra 中的内容是 a 的地址而已,为什么得到的结果会是相同呢?

void TestRef(){
	int a = 10;
	//ra 引用 a,ra 和 a 是同一块内存空间,不管是使用 ra 还是 a 都是在操作同一片空间
	int& ra = a;
	if(&a = &ra){
		cout << "相同" << endl;
	}
	else{
		cout << "不同" << endl;
	}
}

  其实啊,我给大家画一幅图,相信大家就明白了,如如下所示,通过这幅图和上面的解释说明,我相信大家一定搞明白了引用和指针的关系,所以一定不要搞混了哦。
在这里插入图片描述

引用的特性
  1. 引用在定义时必须初始化;
  2. 一个变量可以有多个引用;
  3. 引用一旦引用一个实体,不能再引用其他实体;
void TestRef(){
	int a = 10;
	int b = 20;
	//引用在定义时必须初始化,所以下面这条语句编译时会出错
	// int& ra;
	//一个变量可以有多个引用,所以 ra、rra 都是 a 的引用
	int& ra = a;
	int& rra = a;
	//引用只能引用一个实体,所以下面这条语句不是在引用 b,而是在将 b 的值赋给 ra
	ra = b;
}
常引用

  在定义引用时,在其前面加const,此时该引用就会变成一个常引用,就和指针里的常量指针是一个意思,所用是被引用实体的值不能改变,主要出错的地方会是下面的这几种情况:

  1. 对常量做引用时,要加const
  2. 对常数做引用时,要加const
  3. 作引用时发生隐式转换,在隐式转换的过程中,会用到临时空间来保存常量,此时引用的就是该临时空间的常量,所以要加const
void TestConstRef(){
	const int a = 10;
	//int& ra = a; 该语句编译时会出错,a 为常量
	const int& ra = a;
	
	// int& b = 10; 该语句编译时会出错,10 是常数,是一个常量,所以要加 const 才行
	const int& b = 10;
	
	double d = 12.34;
	//int& rd = d; 该语句编译时会出错,d 在引用时,因为类型不同会导致隐式转换,此时会存在常量,必须加 const 才行
	const int& rd = d;
}
使用场景

做参数
  引用传参和指针传参效果是类似的,形参发生改变,实参也会改变,最常用的就是交换函数,如下图示;在之前我们知道值传递和址传递是有很大区别的,址传递不浪费空间、效率高,但是肯定有小伙伴觉得指针用起来不好用,很容易搞混淆,那么后面遇到这种情况我么就可以使用引用来解决,非常的方便,大家一定要记住哦。

void Swap(int& left, int& right){
	int temp = left;
	left = right;
	right = temp;
}

做返回值
  做返回值时,一定要注意返回值的作用域,如果出了作用域之后,该引用仍然存在,那就是可行的,否则就是不可行的;
在这里插入图片描述

内联函数

概念

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

特性
  1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
  2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
  3. 定义在 class 声明内的成员函数默认是inline函数;
  4. 在同一个项目的不同源文件内定义函数名相同的inline函数时,这些同名函数的实现必须相同,所以一般把inline函数的定义放在头文件中更加合适,放在头文件中必须加inline
  5. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到(较新的编译器应该可以完成链接)。例如如下操作就会导致错误发生:
// F.h
#include <iostream>
using namespace std;
//函数声明
inline void f(int i);
// F.cpp
#include "F.h"
//函数定义
void f(int i){
	cout << i << endl;
}
// main.cpp
#include "F.h"
int main(){
	//调用函数
	f(10);
	return 0;
}
宏与内联
1、宏是实现了简单的替换,虽然有一些优点,但是缺点也很明显,比较如下
宏的优缺点:
  • 优点:
    1.增强代码的复用性。
    2.提高性能。
  • 缺点:
    1.不方便调试宏(因为预编译阶段进行了替换)。
    2.导致代码可读性差,可维护性差,容易误用。
    3.没有类型安全的检查 。
2、按照我们现在所掌握的知识,我们是有办法将宏来替代的,保留它的优点,去除缺点,主要是以下两种方法:
1.常量定义:换用 const
2.函数定义:换用内联函数;

auto关键字

概念

  C++11 中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

int TestAuto(){
	return 10;
}
int main(){
	int a = 10;
	//推导 b 为 int 型
	auto b = a;
	//推导 c 为 char 型
	auto c = 'a';
	//推导 d 为 int 型
	auto d = TestAuto();
	//auto e; 无法通过编译,使用 auto 定义变量时必须对其进行初始化
	//下面是语句打印变量的类型
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	return 0;
}

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

使用细则
  1. auto与指针和引用结合起来使用,在用auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须加&,举个例子将会更好理解:
int x = 10;
//下面 px1 和 px2 都是一样的,都是指向 x 的指针
auto px1 = &x;
auto* px2 = &x;

int y = 10;
//ry1 引用 y
auto& ry1 = y;
//这里 ry2 不做引用,相当于是 ry2 等于 y,所以类型为 int
auto ry2 = y;
  1. 在同一行定义多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto(){
	//正确,类型为 int
	auto a = 1, b = 2;
	//该行代码会编译失败,因为 c 和 d 的初始化表达式类型不同
	auto c = 3, d = 4.0; 
}
  1. auto不能作为函数的参数;
// 此处代码编译失败,auto 不能作为形参类型,因为编译器无法对 a 的实际类型进行推导
//其实就是相当于没有初始化
void TestAuto(auto a)
{}
  1. auto不能直接用来声明数组;
  2. 如想了解的更多,可以看看其他大佬的博客,例如:C++11特性:auto关键字

范围for循环

语法

  之前我们写打印数组循环的时候都是通过下面的方法来进行的:

void TestFor(){
	int array[] = { 1, 2, 3, 4, 5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
		array[i] *= 2;
	for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
		cout << *p << endl;
}

  对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,具体演示如下:

void TestFor(){
	int array[] = { 1, 2, 3, 4, 5 };
	//使用 auto 是方便我们写代码,不用去管数组中数据的类型,系统自动判定
	for(auto& e : array)
		e *= 2;
	for(auto e : array)
		cout << e << " ";
	cout << endl;
	return 0;
}

  在上面的第一个for循环这里,使用引用接受数组数据,不仅使得我们可以修改数组内容,还让程序效率提高,因为没有额外空间开销;如果想要既提高效率又不会改变数组内容,那么我们可以在auto前面加const,这样就可以实现了。
  需要注意的是,如果数组作为参数传入函数,再在函数中使用范围for循环的换,是不可以的,因为数组作为参数,会隐式转换为指针,这样就会丢失范围,系统无法帮我们界定操作开始和结束。

指针空值

  给大家看一个代码,大家来判断一下应该是什么结果:

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

  运行上面的代码,得到的结果是f(int),f(int),f(int*),由此可以看出,以前我们所接触到的NULL,和整型 0 没什么区别,我们查看代码头文件可以得到如下结果:

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

  由上可知,NULL并不是一个真正的空指针,在 C 语言中是将 0 强转为(void*),而在 C++ 中则是字面常量 0(C++ 中不能将void*类型的指针隐式转换成其他指针类型),所以为了避免出现一些错误生成、为了避免混淆,在 C++11 中新定了一个关键字nullptr,用于标识空指针,这是std::nullptr_t类型的变量,它可以转换成任何指针类型和bool布尔类型(主要是为了兼容普通指针可以作为条件判断语句的写法),但是不能被转换为整数,在今后的学习中,更建议大家使用这个,以此来规范我们代码,提高代码的健壮性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值