【C++】C++基础知识

前言

C++是对C语言的完善以及再发展,C++通过增加一些新的语法来弥补C语言的不足,可以看作是C语言的超集,所以C++是兼容C的,也就是说我们在 .cpp 文件中编写、运行 .c 程序,编译器也不会报错,反之则不行;C 和 C++的基本设计理念可以概括为“信任程序员”。C++旨在让程序员可以高度自由地完成他们想要的任务,这意味着新程序员可能会陷入相当多的陷阱,这是**“知道如何正确使用C++和不该怎么用C++两者同等重要”**的原因之一,也是C++被广泛认为难学的原因之一。

一、命名空间简介

1. 什么是命名空间

在C语言中,在同一个域内,函数、变量和类型都是不能重名的,如果我们的程序中包含某一头文件,那么我们就不能定义与其同名的全局变量,否则编译器就会报错。但是C语言头文件中的库函数是非常多的,我们在编写大型项目的时候就难免可能会定义与库函数同名的变量,从而造成命名冲突;为了解决这个问题,C++引入了命名空间。

在这里插入图片描述

命名空间:在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多命名冲突,这种情况就可以使用命名空间把重名的标识符保护起来,就像一个保险箱。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,其中定义命名空间的关键字是 namespace

2. 命名空间的使用

定义命名空间很简单,只需要使用namespace 关键字,后面跟上命名空间的名字,然后后接一对 {} 即可,{} 中即为命名空间的成员。

namespace wei
{
	int rand = 36;

	int add(int a, int b)
	{
		return a + b;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}

命名空间有如下特点:

  1. 命名空间的名称是随意取的
  2. 命名空间中可以定义函数/变量/类型;
  3. 命名空间可以嵌套;
  4. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会将其合成为一个命名空间;

命名空间的使用有三种方式:不暴露成员、暴露部分成员以及暴露全部成员,以下面的命名空间为例:

namespace wei
{
	int rand = 36;

	int add(int a, int b)
	{
		return a + b;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}

1. 不暴露成员

#include<iostream>
int main()
{
    int a = 10;
    int b = 20;
	printf("%d\n", wei::rand); //命名空间名称加作用域限定符:
    printf("%d\n", wei::add(a, b));
	return 0;
}

在这里插入图片描述

这种方式虽然是最麻烦的,但是却是最安全的,不管命名空间里的标识符是否与全局域的标识符重名,都可以用这种方式使用命名空间里的标识符。

2. 暴露部分成员

#include<iostream>
using wei::add;  //使用 using 将命名空间中某个成员引入,这样就可以当作正常的全局标识符使用
int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", wei::rand);
	printf("%d\n", add(a, b));
	return 0;
}

3. 暴露全部成员

#include<iostream>
using namespace wei;  //使用 using namespace 将命名空间名称引入
int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", wei::rand); //对于与头文件中全局函数重名的变量需要用作用域限定符
	printf("%d\n", add(a, b));
	return 0;
}

这种方法是最简单省事的,但同时又是最危险的,因为暴露了全部的成员相当于命名空间毫无保护作用,当你要使用与全局标识符重名的标识符时,仍然需要使用第一种方式。

总的来说,我们想要使用命名空间中的成员,一共有两种方法:一种是使用作用域限定符 ::,另一种是引入命名空间,而引入命名空间又分为部分引入和全部引入。

对于嵌套定义的命名空间,我们逐层使用作用域限定符即可,当然也可以通过逐层引入命名空间的方式使用。

3. 注意事项

1、一个命名空间就定义了一个新的作用域,这个域叫做命名空间域,命名空间中的所有内容都局限于该命名空间域;

2、命名空间中定义的变量都是全局变量;

3、编译器查找变量的规则是:默认先在局部域中查找,如果找不到,再到全局域中去找,如果在全局域中也没找到该变量,就报错;而使用命名空间加作用域限定符的作用就是改变编译器查找变量的规则,让编译器先在局部域中查找,如果找不到,就直接到命名空间中去找,再找不到就报错。

二、基本输入/输出简介

C++的输入输出语句如下:

#include<iostream> //iostream 是C++标准库的一部分,允许我们从/向控制台读取和写入文本
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std; 
int main()
{
	int a = 0;
	cin >> a;  //console in
	cout << a << endl;  //console out endline
	return 0;
}

在这里插入图片描述

1、使用 cout 标准输出对象(控制台)和 cin 标准输入对象(键盘)时,必须包含 头文件 以及按命名空间使用方法使用 std。

2、 cout 和 cin 是全局的流对象,endl 是特殊的C++符号,表示换行输出,相当于‘\n’,他们都包含在头文件中。

3、<< 是流插入运算符,>> 是流提取运算符。

4、使用C++输入输出更方便,不需要像 printf/scanf 输入输出时那样,需要手动控制格式;C++的输入输出可以自动识别变量类型

5、实际上 cout 和 cin 分别是 ostream 和 istream 类型的对象,>> 和 << 也涉及运算符重载等知识, 这些知识我们我们后续才会学习,所以这里只是简单介绍它们的使用。

std命名空间的使用惯例

1、 在日常学习中,建议直接 using namespace std 即可,因为这样很方便。

2、但是项目开发中代码较多、规模大,就很容易出现定义跟库重名的变量/对象/函数,该问题在日常练习中很少出现,所以建议在项目开发中采用像 std::cout 这样使用时指定命名空间 和 using std::cout 来展开常用的库对象/类型两种方式混用。

三、缺省参数

1. 缺省参数的概念

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

在这里插入图片描述

2. 缺省参数的分类

缺省参数分为两类:全缺省参数和半缺省参数

全缺省参数

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

半缺省参数

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

注意事项

  • 半缺省参数必须从右往左依次来给出,不能间隔着给;
  • 缺省参数不能在函数声明和定义中同时出现,如果存在函数声明,那么缺省参数只能在函数声明处给定;
  • 缺省值必须是常量或者全局变量

四、函数重载

1. 缺省参数的概念

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

#include<iostream>
using namespace std;

//参数类型不同构成函数重载
int Add(int left, int right)
{
	return left + right;
}

double Add(double left, double right)
{
	return left + right;
}

//参数个数不同构成函数重载
void func()
{
	cout << "func()" << endl;
}
void func(int a)
{
	cout << "func(int a)" << endl;
}

//参数顺序不同构成函数重载
void f(int a, char b)
{
	cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
	cout << "f(char b, int a)" << endl;
}

int main()
{
	Add(10, 20);
	Add(10.1, 20.2);
	f();
	f(10);
	f(10, 'a');
	f('a', 10);
	return 0;
}

在这里插入图片描述

2. 缺省参数的底层原理

一个程序要运行起来,需要经历预处理、编译、汇编、链接四个阶段;其中编译阶段会进行符号汇总,汇编阶段会生成符号表,而链接阶段则会对符号表进行合并与重定位,其中符号表会将每一个变量都关联上一个地址,但这个地址是否有效需要在链接阶段进行符号表的合并与重定位时才能检查出来。

在这里插入图片描述

对于上述过程中形成符号表这一阶段,C编译器与C++编译器所进行的操作是不同的。C语言编译器会直接用函数名作为符号表中的符号,就像上图一样,所以C语言中函数名相同不会构成函数重载,反而会在链接阶段报错;而C++编译器则是会根据函数参数对函数名进行修饰,保证参数列表不同修饰出的函数名就不同,用修饰后的名称来构成符号表,这样在链接阶段就不会产生冲突。

由于Windows平台vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,所以下面我们使用g++来演示这个函数名修饰规则

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

在这里插入图片描述

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

在这里插入图片描述

经过对比可以发现:在linux平台,采用 gcc (C语言编译器) 编译完成后,函数名没有发生改变;而采用g++ (C++编译器) 编译完成后,函数名发生改变,函数名由前缀_Z + 函数名长度 + 函数名 + 参数类型首字母组成,即编译器将函数参数类型信息添加到了修饰后的名字中。

通过上面这个实例我们就可以理解:C语言没办法支持函数重载是因为同名函数没办法区分;而C++是通过函数名修饰规则改变了函数名来区分,只要参数不同,修饰出来的函数名就不一样,所以就支持重载。

同样,我们也理解了函数的返回值类型不同是不构成函数重载的,因为函数的返回值类型不参与修饰函数名;但其实即使是C++编译器把函数的返回值类型也加入了函数修饰规则,也仅仅是让它在语法层面构成了重载而已,在实际使用中也是不构成重载的,因为函数调用时并不会传递函数的返回值类型,所以函数调用产生的符号表根本不可能有返回值类型参与函数名修饰,所以会在链接阶段报错。

五、内联函数

1. 内联函数的概念

一个函数在调用时会建立函数栈帧,结束调用时会销毁函数栈帧,而函数栈帧的建立与销毁是有空间和时间上的开销的;那么,对于功能简单、调用次数非常多的函数来说,每次调用都重新开辟栈帧势必就会造成效率的降低;比如快速排序的 hoare 法,仅仅是每次单趟排序都会调用很多次 Swap 函数,更别说单趟排序也会被递归调用很多次,而 Swap 函数本身的功能又十分简单,那么该如何来对其进行优化呢?

在C语言中,我们可以使用宏函数来解决这个问题:我们直接将 Swap 函数写成宏函数,这样程序在预处理阶段会直接将 Swap 函数替换成相应的代码,从而不再建立函数栈帧。

//源代码
#include <stdio.h>
#define Add(x,y) ((x)+(y))  //宏函数
int main()
{
	int ret = Add(2, 3);
	printf("%d\n", ret);
}

//经过预处理之后的代码
{
    //...此处是 stdio.h 展开的内容
}
int main()
{
    int ret = ((2)+(3));
    printf("%d\n", ret);
}

但是宏函数有两个缺点:一是宏不能调试,二是宏没有类型安全检查

基于C语言宏函数的这些缺陷,C++设计了内联函数

用 inline 关键字修饰的函数叫做内联函数,编译时C++编译器会把函数体在调用内联函数的地方展开,没有函数调用建立栈帧的开销,可以提高程序运行的效率;

内联函数的编写和正常函数一样,仅仅是在函数的返回值类型前添加一个 inline 关键字 ,这样就解决了C语言宏函数容易写错以及没有类型安全检查的缺陷,同时还支持调试

#include <iostream>
using namespace std;

inline int add(int a, int b)
{
	return a + b;
}

int main()
{
	int a = 10;
	int b = 20;
	cout << add(a, b) << endl;
	return 0;
}

但是在 debug 模式下,内联函数不会自动展开,需要我们对编译器进行相关设置;在 release 模式下,内联函数会自动展开,但是却不能调试了;

debug模式下汇编代码显示正常调用函数

在这里插入图片描述

release模式下汇编代码显示没有调用函数,也没有建立函数栈帧

在这里插入图片描述

2. 内联函数的特性

特性1

《C++ primer》第五版对于内联函数的介绍:

在这里插入图片描述

特性2

内联函数是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段就会用函数体替换函数调用;这样做的优势是减少了栈帧建立的开销,提高了程序运行效率;缺陷是可能会使目标文件变大。

需要注意的是,这里的空间指并不是指程序运行时占用的内存空间,而是指经过编译链接后得到的可执行程序 (.exe/.o文件) 所占用的空间。普通函数转换得到的指令只会在定义的地方有一份,调用的地方只有一条call指令,而内联函数会在所有调用函数的地方展开函数体,转换得到一份完整的函数执行指令。如果函数调用的次数非常多,那么转换得到的代码指令就会非常多,这其实就是所谓的 “代码膨胀”,这也在一定程度上解释了为什么当内联函数过长时编译器不进行展开。

特性3

内联函数不建议声明和定义分离,否则会导致链接错误,具体原因如下:

首先要知道程序在编译阶段进行符号汇总,汇编阶段生成符号表链接阶段进行符号表的合并和重定位

  • 对于定义在同一文件中的函数来说,编译器在汇编阶段会直接调用该函数,在调用过程中会生成对应的符号表,且此符号表中的地址一定是有效的,所以程序不会进行后续的链接操作;

  • 对于定义在不同文件中的函数,编译器会先在函数调用所在文件内寻找该函数的声明,在汇编阶段生成的符号表中的地址是无效的,此时编译器会继续后续的链接操作;链接阶段符号表的合并会将不同文件在汇编阶段生成的所有符号表合并到一起,合并的意思是如果两个符号表中的函数名相同,那么编译器会选取与有效地址相关联的符号表,也就是有函数定义的文件生成的符号表,丢弃掉另一个与无效地址关联的;这样同时具有声明和定义的函数经过合并就只有一个符号表了,重定位时编译器就会根据有效地址来调用函数,这样就可以实现跨文件调用函数;

链接错误:如果一个函数只有声明,没有定义的话,那么在汇编阶段就不会生成与有效地址相关联的符号表,在链接阶段经过符号表的合并之后关联的仍然是一个无效地址,则在进行符号表的重定位时就会发生链接性错误。

对于内联函数来说,如果我们将函数的定义和声明分离,那么函数的声明会在汇编阶段生成一个符号表,里面关联的是一个无效的地址;但是由于内联函数是直接展开的,所以函数定义部分在汇编阶段并不会生成符号表,相当于没有函数定义,这时候就会出现上面的链接错误。

在这里插入图片描述

正确用法:如果有头文件,将内联函数的定义直接放在头文件中;如果没有头文件,就直接放在本文件内部

在这里插入图片描述

六、auto关键字(c++11新特性)

1. auto的概念和用法

在早期C/C++中,使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是没有多少人去使用它;因为当函数调用结束后,函数的栈帧会被销毁,那么存在于函数栈帧中的局部变量自然也会被销毁,这就使得 auto 修饰失去了意义;

在C++11中,C++标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量类型由编译器在编译阶段由初始化表达式自动推导而得,所以使用auto定义变量时必须对其进行初始化。

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

	//声明指针类型时,auto和auto*没有任何区别
	auto d = &b;
	auto* e = &c;
	auto& f = a; //auto声明引用类型时则必须加&
	
	auto x = 10, y = 20; //在同一行声明多个变量时,这些变量必须是相同的类型
	//auto m = 'a', n = 30;  无法通过编译
	//auto e;  //无法通过编译,使用auto定义变量时必须对其进行初始化
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	cout << typeid(e).name() << endl;
	cout << typeid(f).name() << endl;
	return 0;
}

在这里插入图片描述

注意事项

  1. 使用auto定义变量时必须对其进行初始化

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

  3. 在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量;

  4. auto不能声明函数的形参,因为不是所有的参数都有初始化表达式,因此编译器可能无法推导出a的实际类型,所以直接规定auto不能作为函数形参,但是可以作为函数的返回值。

在这里插入图片描述
  1. auto不能直接用来声明数组,因为数组名是一个指针,因此 auto 无法推导元素的类型

  2. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法

3. auto的使用场景

1. auto自动推导类型

在代码编写过程中,随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在类型很长,难以拼写以及含义不明确,容易出错,比如下面变量m的类型就十分复杂:

#include <string>
#include <map>
int main()
{
	std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
	std::map<std::string, std::string>::iterator it = m.begin();
	while (it != m.end())
	{
		//....
	}
	return 0;
}

std::map<std::string, std::string> 是一个类型,但是该类型太长了,特别容易写错,我们可以通过 typedef 给类型取别名,但是 typedef 有时会遇到新的难题:

在这里插入图片描述

这个时候就可以用auto自动推导类型,它完全避免了 typedef 的缺陷,却可以实现相似的功能。

2. 基于范围的for循环

auto在实际中最常见的用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。

在C++98中,要遍历一个数组需要程序员自己控制遍历范围,但是对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还容易犯错误;因此 C++11 中引入了基于范围的for循环。for循环后的括号被冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,也就是数组名。for循环迭代的范围必须是确定的

int main()
{
	int nums[] = { 1, 2, 3, 4, 5 };
	for (auto& i : nums) //引用遍历
	{
		i *= 2;
	}
	
	for (auto i : nums)
	{
		cout << i << " ";
	}
	return 0;
}
在这里插入图片描述

七、空指针nullptr(c++11)

在C语言中,通常我们在定义一个指针变量的时候会将其初始化为 NULL,避免未初始化就被解引用造成野指针越界访问问题。其实这里的 NULL 是C语言中定义的一个,在传统的C头文件 (stddef.h) 中,可以看到如下定义:

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

我们可以看到,在C++中,NULL被定义为数字0;在C语言中,NULL 其实是数字0被强转为指针类型,相当于值为0的指针;虽然 0 和 (void*)0 二者在数值上相同,但是他们的类型是不同的

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;
}
在这里插入图片描述

可以看到在C++中 NULL 确实被定义为数字0,要想使用空指针,必须对其进行强制类型转换。为了方便使用空指针,C++11中专门为空指针设计了一个关键字 – nullptr,可以认为 nullptr 就是 (void*) 0

注意事项

  • 在使用 nullptr 时不需要包含头文件,因为 nullptr 是C++11作为新关键字引入的;
  • 为了提高代码的健壮性,在后续表示空指针时最好使用nullptr;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值