C++入门基础知识

摘抄文段:
我们应当相信,每个人都是带着使命来到人间的。无论他多么的平凡渺小,多么的微不足道,总有一个角落会将他搁置,总有一个人需要他的存在。

C++的第一个程序

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello World\n" << endl;
	return 0;
}

对于这个程序,可能会有几个疑问,比如:

  1. 包含的库是<iostream>,这个库的作用是什么?
  2. 为什么和C语言的相比,会多出
    using namespace std; 这行代码?
  3. cout 和 << 是怎么搭配使用的?
  4. endl 又是什么意思?
    接下来,这些问题会被一 一解答

<iostream>的含义与作用

  <iostream> 是Input Output Stream 的缩写,看到这个名字,大家可能会联想到C语言的 <stdio.h> 标准输入输出库。
  没错,<iostream> 库确实包含了C++的一些输入输出函数,这也是为什么程序需要包含它的原因。cout函数声明也需要去包含该库。

命名空间

namespace的意义

   C++中,变量、函数等都是大量存在的。这些变量。函数的名称将都存在于全局作用域中,可能会造成很多冲突。比如同名称的函数会造成冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是来解决这个问题的。
例如以下:
图片1
从图中我们可以看到两个同名称的函数都叫 func ,返回值和参数列表也都相同,但是程序可以不报错且正常运行。
体现在以后的大型项目集合时,我们把变量和函数装在自己的 namespace 中,需要使用时就调用,和自己本地代码区创建的变量也不会产生冲突。
即使是两个人namespace 中有同名的变量或者函数,依旧不会产生冲突,调用时使用规定的 域作用限定符(::) 即可调出想要使用的变量或者函数。

命名空间的定义

使用

namespace 命名空间的名字
{
  //写入需要定义的变量和函数
}  //注意花括号的后面不需要加分号( ;
namespace Test { int a = 10; double b = 15.6; char str[] = "Hello World"; }

命名空间也可以嵌套,如:

namespace Test
{
	namespace test1
	{
		int a = 10;
		double b = 15.6;
		char str[] = "Hello World";
	}
	namespace test2
	{
		int a = 20;
		double b = 25.6;
		char str[] = "Keep doing";
	}
}

使用时,使用两次(::)域作用限定符

图片6

同名的namespace会自动合并为一个namespace:

图片

符号"::" 域作用限定符

  使用方法:(指定的命名空间)::(变量或者函数)
std 是 C++中的一个命名空间,使用cout函数就需要从std 中限定出来.

std::cout图片2

using 的使用

using 可以将命名空间中的某个成员展开,也可以展开整个命名空间,如:
前者:

using Test:: a  将成员展开后,这个变量名将在展开的作用域中存在。
图片3

如果是这种形式(将成员展开在函数作用域中):
图片4> 如何访问到全局变量中的a变量,使用(::)域作用限定符,但是前面不需要加命名空间,就可以调用全局变量中的a变量。
如果不这样做,一般函数中会调用最近的局部变量。

接着是展开全部的命名空间
命名空间中的所有成员都会被展开,可以直接当做变量去访问,当然也可以通过(::)域作用限定符来访问。
图片5

using namespace std;

  到了这里,估计大家应该能理解这行代码的含义了。
using 是为了展开 命名空间(namespace) std ,而cout 和 endl的使用都需要 命名空间 std 来访问。

建议与注意

  三种方式使用命名空间:

  1. 指定命名空间访问,项目中推荐这种方式。

就是使用变量和函数都是用域作用限定符来访问,而不是使用using来展开命名空间
如 std::cout,  std::cin ,  std::endl;

  1. using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
  2. 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小训练程序为了方便推荐使用。

如:using namespace std; 会展开 std命名空间中的所有变量名和函数,可能会导致命名冲突或者名字污染。

cin 、cout 、endl

  1. std::cin 是istream类的对象,它主要面向窄字符(narrow characters(of type char))的标准输入流
  2. std::cout 是ostream类的对象,它主要面向窄字符的标准输出流。
  3. std::endl是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
  4. << 是流插入运算符如cout << "Hello World";就像是右边的字符串流了过来,从右往左流到cout中输出
    >> 是流提取运算符如cin >> a;就像是输入从cin中流出,从左往右流到变量之中去,像是数据的流动方向。

如果上面的没看懂,其实知道他们的作用就行了。

cin 就是读取用户的输入,而且不需要补上数据的类型,它会自动识别变量类型。
如下面这个变量a,使用cin,不需要像C语言的scanf需要指定输入的格式,而C++会自动识别变量的类型然后赋值进去。
图片

cout 就是输出内容。

endl 就是给输出添加换行。

cout / cin / endl 等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去使用它们。

缺省函数

  缺省函数是声明或定义函数时为函数的参数指定一个缺省值。缺省参数分为全缺省和半缺省参数,某些地方也把缺省参数叫做默认参数。意思就是在调用缺省函数时,如果没有指定实参去传数据,则采用该形参的缺省值。
如下,如果不给形参a传入数据,则形参a的值为缺省值10:

void func(int a = 10)
{
	cout << a;
}

图片

全缺省和半缺省

  全缺省就是全部形参给缺省值,半缺省就是不跟形参给缺省值,也就是只要不是所有形参都给缺省值就是半缺省。
同时C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值,不能是想哪个缺省就哪个缺省。
正确案例:

void func(int a,int b = 10,int c = 20, int d = 30)
{}

错误案例:

void func(int a = 1,int b,int c = 20,int d = 30)
{}
void func2(int a = 1, int b = 10;int c,int d)
{}

带缺省参数的函数调用

  C++规定必须从左到右依次给实参,不能跳跃给实参。就是不能我就指定传哪个参数,而其他参数不选择传参,一定是按着顺序的去给参数。
如:
图片

函数声明和定义分离时的注意事项

缺省参数不能再函数声明和定义中同时出现,规定必须函数声明给缺省值。
图片

函数重载

C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。

函数可以重载的情况

参数个数不同

同名称函数的参数个数不同可以构成函数重载。

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

图片

参数类型不同

同名称函数的参数类型不同(即使是参数个数相同)也可以构成函数重载。

void swap(int* a,int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}
void swap(double* a,double* b)
{
	double temp = *a;
	*a = *b;
	*b = temp;
}

图片

参数类型顺序不同

同名称函数的参数类型顺序不同(即使参数个数相同)也可以构成函数重载。

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

图片

重载函数的注意事项

返回值不同不能作为重载条件。
因为调用时无法区分,到底是有返回值的函数还是无返回值的函数。

void Print()
{
	cout << "Keep doing" << endl;
}
int Print()
{
	cout << "Keep doing" << endl;
	return 0;
}

图片

除此之外,还有缺省函数与无参函数之间虽然能构成函数重载,但是在调用时会报错,存在歧义,编译器不知道调用谁。

void func()
{}
void func(int a = 10)
{}

图片

PS:个人感觉在不构成歧义的情况下,只要函数的参数列表发生了一点变化,都可以构成函数重载。如上述所讲:参数的个数发生变化,参数的类型发生变化,参数的顺序发生变化。

引用

引用的定义

引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间。
使用方法:
类型& 引用别名 = 引用对象

int main()
{
	int a = 10;
	// 引用:b和c是a的别名
	int& b = a;	
	int& c = a;	
	// 也可以给别名b  取别名,d相当于还是a的别名
	int& d = b;
	return 0;
}

图片

typedef、 #define、 &(引用)的区别

typedef: 用于对类型名重命名
在这里插入图片描述

#define:是文本代替,用于编译阶段,对#define的符号进行替换,本质上就是使用者用熟悉的符号去代替一些已知的常量符号。
#define MAX 99999999;
在编译阶段,把符号位MAX的文本都替换为 99999999

&:引用是给变量起别名。
int a = 10;int& b = a;

引用的三点注意

引用在定义时必须初始化

int main()
{
	int a = 10;
	int& b;	//这是错误的
	return 0;
}

图片

一个变量可以有多个引用

下面代码是可以运行的。

int main()
{
	int a = 10;
	// 以下都是可以的
	int& b = a;
	int& c = a;
	int& d = a;
	return 0;
}

引用只能指向一次,一次指向后不能改向

引用一个实体后,没有办法去引用其他实体。分析下列代码:

int main()
{
	int a = 10;
	int& b = a;

	int c = 20;
	b = c;
	// 请问现在b是多少,a又是多少?
	cout << "b=" << b << ",a=" << a << endl;
	return 0;
}

图片
此时,b=c;这行代码已经不能改变b的指向,只能是b这个别名,对a变量所对应的内存进行了操作,即赋值操作。
所以a变量 和 b变量所对应的值为 c的值(即20).

所以在一些方面,指针具有不可替代性,如链表中,指针可以多次指向,而引用只能引用一个实体,再不能引用其他实体

引用的作用

  引用在实践中主要用于引用传参和引用做返回值中减少拷贝用于提高效率,和改变引用对象的同时改变被引用对象。

  • 引用传参和引用做返回值,可以减少拷贝,提高效率
  • 改变引用对象的同时改变被引用对象

图片

为什么会提高效率?是因为传值返回 也会产生临时空间(即临时对象)来储存返回值,为了减少拷贝和开辟空间的代价,使用返回 引用(即返回别名),可以做到减少拷贝,直接返回一个引用变量。
同理,也是为什么采用引用传参的原因。可以想象,如果传入一个长度为1000的数组。采用传值调用,那么就要开辟空间并拷贝1000个数据,这就是传值调用的成本,但是引用传参并不需要去开辟空间,而是使用原本的内存空间。
感觉引用传参和指针传参功能是类似的,但是引用传参更方便一些,所以在C++中,引用不是为了代替指针,而是为了解决在某一些方面对于指针来说比较难以理解的问题,使用引用会让逻辑更加清晰(即辅助指针在某一方面的应用)。

const引用

  首先我们得知道const引用可以引用const对象,也可以引用普通对象和临时对象。

  1. 我们知道const对象就是用const修饰的变量。
	const int a = 10;
	const int& ra = a;
	// 以此类推,更换类型名即可
  1. 普通对象不是const修饰的变量,但是同类型的变量。
	int a = 10;
	const int& ra = a;
	
	double b = 20.56;
	const double& rb = b;
  1. 临时对象产生于类型转换中储存中间值,表达式运算储存结果。
	int a = 10;
	const int ra = a*5;
	
	// 隐式转换,从double转换为int
	double b = 20.56;
	const int& c = b;

注意:C++规定临时对象具有常性。在类型转换中会产生临时对象储存中间值,也就是引用的对象如果是临时对象,就需要使用xonst引用,因为临时对象具有常性。
除了类型转换,表达式计算出来的东西也是临时对象。

所谓临时对象就是编译器需要一个空间去暂存表达式的求值结果时临时创建的一个未命名的对象,C++中就把这个未命名对象叫做临时对象。

关于变量权限问题

  权限可以看作是变量对一块内存空间的操作能力
  变量对内存空间有读取和写入的权限,在这个基础上,我们可以砍去写入的权限,保留读取的能力。使用const就可以砍去写入的权限。
  可以想想,我们用const修饰后的变量相当于常量,我们只能读取,不能再进行写入操作。

权限放大问题

  权限不能放大,则必须使用const引用 const变量
如下:

这是原变量内存空间的权限问题

图片

下面这个就是临时对象具有常性,导致 int& ra 引用临时对象时必须要加const修饰
图片

复习一下:const修饰指针
记住口诀:左定值右定向,const修饰不变量
const修饰在指针的左边,代表指针所指向的变量,通过指针解引用不能改变它的值,但是可以改变指针的指向。

图片

图片

const修饰在指针的右边,代表指针不能改变指向,但是可以改变指针所指向的变量的值。

图片

图片

所以指针有两个地方可以改变,一个是指针的指向,一个是指针通过解引用改变指向变量的值。

权限不能放大,但是可以缩小

  虽然指针不能放大,但是缩小可以。
  也就是变量本来具备读写的权限,但是我使用 const 引用只获取该变量的读取权限,不需要它的写入权限。
  记住,引用相当于得到该变量的一个别名,使用const引用就是得到一个只能进行读取操作的别名。

图片

回忆一下,权限放大和缩小问题是否有所了解了。
接下来,看一段代码,看看这段代码有没有问题:

	int a = 20;
	int* p1 = &a;
	const int* p2 = p1;

	int* const p3 = &a;
	int* p4 = p3;

其实上面这段代码没有什么问题,如果觉得 p4 有改变 p3 的指向,这应该是不正确的。int* p4 = p3;这句代码只是进行了赋值操作,把 a 变量的地址赋值给了 p4 指针。下面有张图,可以看看。

图片

const引用的好处

  回归开头,const引用可以引用 const对象,也可以引用普通对象和临时对象。
疑问:const引用可以接受更多的对象,但是为什么不用传值调用呢?
还是考虑到传值调用需要拷贝会浪费更多资源,而且在函数模版等一方面,函数需要接受更多的对象,所以 const引用性价比更高。虽然 const引用不能改变被引用对象的值。
如图:
图片
注意:const修饰是可以接受更多的对象(在不要求改变形参的值的前提下,毕竟const修饰后不能改变其的值),引用是为了减少拷贝,提高效率。

指针和引用的关系

  指针像是哥哥,引用像是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。

  • 从语法概念上,引用是一个变量的别名,不需要开空间;而指针是储存一个变量的地址,要开空间。
  • 引用不会出现空引用,因为引用在定义时必须要初始化;而指针会出现空指针,把指针置空即可。
  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断地改变指向的对象。
  • 引用可以直接访问被引用的对象;而指针需要解引用才是访问指向的对象。
  • 使用 sizeof 来得到其空间大小。引用的结果为引用类型的大小;而指针始终是地址空间所占字节个数(32位平台下占4个字节,64位平台下占8个字节)。
  • 指针容易出现野指针,而引用也有可能出现野引用的情况。

仅做了解
  实际上在底层汇编语言,引用也是用指针实现的。
  在函数栈帧销毁后,返回的野引用(因为函数销毁,函数里所包含的局部变量也已被销毁)依然可以改变它的值。这是因为实际上底层引用是使用指针写的,相当于返回一个野指针,并且对野指针进行了修改,但是越界写不一定报错,才导致程序正常运行。

inline

  • 用 inline 修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。
  • inline 对于编译器来说只是一个建议,也就是说,你加了 inline 编译器也可以选择在调用的地方不展开。不同的编译器关于 inline 什么情况展开各不相同,因为C++标准没有规定这个。
  • VS2022编译器Debug版本下,是默认不展开 inline 的,这样方便调试,因为 inline 展开的话,内联函数像C语言的宏函数一样,把代码直接铺开在调用的地方,这样是无法进行调试的。debug版本想要展开需要设置一下下面两个地方:

打开项目的属性选项卡
图片

在这里插入图片描述

  • inline 不建议把声明和定义分离到两个文件,分离会导致链接错误。因为inline 如果被展开,那么就没有函数地址,链接时就会出现报错。

在这里插入图片描述

  • inline 适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,即使加上 inline 也会被编译器忽略。

短小函数图片

这样长的函数,编译器也已经不再展开,因为代码量会增大,假设有1000处调用该函数,那么代码长度就是1000 * (函数内包含的代码长度),而使用函数栈帧会是 1000 + (函数内包含的代码长度) 。代码量的增大会引起文件的大小会增大。
图片

C++的内联函数和C语言的宏函数

   C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错,且不方便调试,C++设计了 inline,目的就是替代 C 的宏函数。
  请回忆一下使用C语言的 #define 来设计一个两数相加的Add定义宏,应该怎样设计。
补充一下宏的申明方式:

  • #define  name( parament-list )  stuff
    其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
    注意: 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
    举例: #define SQUARE(x) ((x) * (x))

以下三种都是不正确的,或者说各有各的毛病:

#define Add(int a,int b) return a + b;
#define Add(a , b) a + b;
#define Add(a , b) (a + b)

宏定义中参数的类型不需要标注,宏是允许把参数替换到文本中,只是一种文本替换,所以加上类型后,会导致代码无法被编译器理解。且return在这里也是错误的,因为这只是简单的文本替换,指的是Add这个符号转换成
return a + b;
正确的宏实现应该如下:

#define Add(a , b) ((a) + (b))

想三个问题:

  1. 为什么不建议加分号( ; )?
  2. 为什么要加外面的括号?
  3. 为什么要加里面的括号?

我的回答如下:
4. 因为在C语言中,每行代码写完后都要求加分号( ; ),所以当宏实现时加上了分号,会在使用该宏时,出现两个分号。一个或者两个分号所导致的问题可以看一下,下面这两张图:

图片

图片

  1. 在外面加括号是为了保证宏替换后,宏函数的优先计算。就是为了保障我们的宏函数先运算,再和外面的变量运算。

图片

  1. 在里面加括号,也是为了保证宏函数符合我们的预期输出。宏替换后,我们需要注意宏函数内的参数运算的优先级是否符合预期。

图片

总结:宏函数的实现,需要加一个最外面的括号,同时为了保证参数是表达式时先计算参数出来,每一个参数也都要加上括号。
#define Add(a , b) ((a) + (b))

inline 的使用

  所以inline可以有效解决宏函数这些需要让人特别注意的问题。当你需要一个两数相加的宏函数,就可以像函数一样去定义。编译器会自己识别并选择展开你的内联函数。

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

nullptr

  先来看一段代码,然后猜一下在三次函数调用中,每次调用的函数是哪个。

#include <iostream>
using namespace std;
void func(int x)
{
	cout << "func(int x)" << endl;
}
void func(int* ptr)
{
	cout << "func(int* ptr)" << endl;
}
int main()
{
	func(0);
	func(NULL);
	func((int*)NULL);
	return 0;
}

答案公布:
图片

实际上NULL是一个宏,我们可以用VS2022,按住 ctrl键点击 NULL 可以看到如下代码:

图片

  • C++中的NULL可能被定义为字面常量0,或者C语言中被定义为无类型指针( void* )的常量。
  • 不论采用何种定义,在使用空值(即NULL)的指针时,都不可避免的会遇到一些麻烦,本想通过 func(NULL) 来调用指针版本的 func(int*) 函数,但是由于NULL被定义成0,调用了 func(int x),因此与程序的初衷相悖。func((void*)NULL) 会报错,需要我们定义一个对应的函数才能正常运行。

图片

  • C++11中引入了nullptr,nullptr 是一个特殊的关键字,nullptr 是一种特殊类型的字面量,它可以转换为任意其他类型的指针类型。使用 nullptr 定义空指针可以避免类型转换的问题,因为 nullptr 只能被隐式转换为指针类型,而不能被转换为整数类型。

把 func( void* ptr ) 函数注释掉是因为,两个函数都能被 func(nullptr)调用,所以为了能正常运行,所以注释掉了该函数。

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值