C++入门

C++入门

1:C++关键字

以下关键字仅在C++98这个标准中

在C++98标准中 C++有63个关键字 C语言有32个关键字

C++入门_第1张图片

2:命名空间

概念:在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作 用域中,可能会导致很多冲突。

使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字 污染,namespace关键字的出现就是针对这种问题的。

 

//命名空间情况1
namespace N1  //namesapce是命名空间的类型  N1是命名空间的名字 
{
	int a = 10;//N1命名空间成员
	char b = 'C';//命名空间成员

	int Add(int a, int b)//命名空间成员
	{
		return a + b;
	}
}

//命名空间情况2
//循环嵌套的命名空间 在N2这个命名空间中嵌套了一个名为N3的命名空间
namespace N2
{
	int a = 20;//N2命名空间中的a变量
	double b = 100;
	namespace N3
	{
		int a = 30;//N3命名空间中的a变量
		double b = 200;
	}
}

//命名空间情况3
namespace N1//最终这个N1会和上面的N1被合并起来 变成一个N1 里面的内容也会被包括到新合并的N1中
{
	//int a = 20;//这样是错误的 因为合并起来后 有两个a变量 就重复定义了
	//double b = 30;//即使变量的类型不同 也不可以 因为在引用的时候 会不知道引用的是char类型的还是doubke类型的

	void Print(void)
	{
		printf("哈哈");
	}
}

1:命名空间其实可以看成是一些不同的使用区域 每个命名空间的变量必须由该命名空间去调用

如 N1::a 是等于10的 而不会去调用N2或者N3命名空间中的a

2:同一个工程中可以存在嵌套的命名空间 也可以存在同名的命名空间 但是同名字的命名空间最终会被合并成一个命名空间 所以 同名的两个或者N个命名空间中不能存在变量名相同的变量。

3:不同的命名空间中可以存在相同变量名的变量 但是每次引用的结果由其所在的命名空间决定

 

2.1:命名空间的使用

 

//情况1
namespace N1  //namesapce是命名空间的类型  N1是命名空间的名字 
{
	int a = 10;//N1命名空间成员a等于10
	char b = 'C';//命名空间成员

	int Add(int a, int b)//命名空间成员
	{
		return a + b;
	}
}

int a = 100;//定义全局变量a等于100

int main(void)
{
	int a = 50;//定义局部变量a等于50

	printf("局部变量a等于:%d\n", a);//输出50
	printf("全局变量a等于:%d\n", ::a);//输出100
	printf("N1命名空间变量a等于:%d\n", N1::a);//输出10

	return 0;
}

1:在这里使用了一个 :: 号 这个符号叫做作用域运算符 比如说你定义了一个局部变量A 也定义了一个全局变量A 在要求不修改名字的前提下 让你输出全局变量 那么就可以使用 ::A 这样就代表当前的A是全局变量A

2:同时 输出N1命名空间中的a也是这个原理 使用N1::a表明 当前这个a变量是N1这个命名空间中的

namespace N1  //namesapce是命名空间的类型  N1是命名空间的名字 
{
	int a = 10;//N1命名空间成员a等于10
	char b = 'C';//命名空间成员

	int Add(int a, int b)//命名空间成员
	{
		return a + b;
	}
}

int main(void)
{
	//printf("N1命名空间变量a等于:%d\n", a);这样是错误的 必须表明在哪个命名空间
	return 0;
}

1:不可以直接使用用变量名引用某个命名空间中的值

除非是如下情况

namespace N1  //namesapce是命名空间的类型  N1是命名空间的名字 
{
	int a = 10;//N1命名空间成员a等于10
	char b = 'C';//命名空间成员

	int Add(int a, int b)//命名空间成员
	{
		return a + b;
	}
}

//using N1::a; 方法1 使用using将N1中的a变量引入
//using N1::Add; 方法1 使用using将N1中的Add函数引入  引入不要加参数
using namespace N1;//方法2 使用using将N1这个命名空间引入

void fun1(void)//在其他函数中也可以使用N1中的a 但是using重新引入必须在函数之前
{
	printf("%d\n", a);//输出10
}

int main(void)
{
	fun1();
	printf("N1命名空间中a的大小是:%d\n", a);//输出10
	return 0;
}

方法1和方法2都可以在都可以使得命名空间N1中的成员a变量在其他函数中直接使用变量名 但是有三点需要注意

1:using对变量或者命名空间的引入 必须在函数之前

2:这个时候 全局变量或者局部量中不能出现同变量名的变量 不然就会出错 如下

3:using N1::a; 最好是在某个函数中需要大量使用该空间的某个变量的时候 使用该方式

using namespace N1;最好是我需要大量的用到该命名空间的大部分成员

namespace N1  //namesapce是命名空间的类型  N1是命名空间的名字 
{
	int a = 10;//N1命名空间成员a等于10
	char b = 'C';//命名空间成员

	int Add(int a, int b)//命名空间成员
	{
		return a + b;
	}
}

//using N1::a; 方法1 使用using将N1中的a变量引入
using namespace N1;//方法2 使用using将N1这个命名空间引入


//int a = 20;//不能定义同名的变量名a

int main(void)
{
	int a = 30;//可以存在局部变量a  但是这个时候 函数中输出的a的值就会变成30
	printf("N1命名空间中a的大小是:%d\n", a);//输出30
	return 0;
}

1:如果出现同名的全局变量a 那么程序就会出错 出错原因是变量名重定义

2:如果是出现同名的局部变量a 那么程序不会出错 但是在函数中输出的时候 a的值大小就是30

 

3:C++的输入和输出

使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空 间。

注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文 件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用 +std的方式。

# include 
using namespace std;//如果加上这个 就不需要加std::了

int main(void)
{
	int a = 0;
	int b = 20;

	std::cin >> a;//输入  因为std是包含在std这个命名空间里的 所以要假设空间名和作用域运算符

	std::cout << a;//输出a的值 因为cout是包含在std这个命名空间里的 所以要假设空间名和作用域运算符


	return 0;
}

 

1:为什么不使用printf和scanf 因为cin和cout在使用上是由于标准printf和scanf的 而且printf和scanf是不定参数的函数 printf函数的输入参数是不定的 输出的参数是要输出的字符个数

2:但是如果输出的时候 要求的格式太多 可以使用printf 好控制

附加知识:不定参数的函数

C语言引入了三个宏来处理不定参数的问题,如下所示

//va_arg()、va_start()、va_end()
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1))
 //类型n的大小,这是考虑了字节对齐
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) 
//ap指向第一个不定参数地址
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 
//下一个参数地址  返回当前ap指向的值,并且增加ap
#define va_end(ap) ( ap = (va_list)0 )
 // 将指针置为无效

type va_arg(va_list argptr, type);//返回第一个参数的地址,并接收第二个参数地址
void va_end(va_list argptr);//用于将指针置之无效
void va_start(va_list argptr, last_parm);//用于接收第一个参数

通过上述宏的时候,可以将函数传入堆栈的参数通过指针读取出来(通过va_start和va_arg传入的参数类型作为指针的偏移量,即可将堆栈中存放的数据取出)

# include 
# include 

int sum(int a, ...)//后面的...表示不定参数 必须跟着第一个参数之后
{
	va_list POINT;//定义指针变量POINT
	va_start(POINT, a);//
	int SumVal = a;
	
	a = va_arg(POINT, int);
	while (a != 0)
	{
		SumVal += a;
		a = va_arg(POINT, int);
	}
	va_end(POINT);
	return SumVal;
}

int main(void)
{
	int i = sum(1, 2, 3, 0);//将4个参数传入sum函数
	printf("使用不定参数得到的值是:%d\n", i);//输出的结果是6

	return 0;
}

在sum函数的参数列表中,如果要做到不定参数,那么就要在第一个有效参数之后加上三个... (只能是三个) ...表示不定参数,必须跟着第一个参数之后

1:使用va_list POINT 定义了一个va_list指针 POINT

2:调用va_start函数 这个时候 POINT指向了参数1

3:在把第一个参数的值赋值给sumval之后 调用va_arg函数 这个时候va_arg中的POINT指针返回了第一个参数的地址 接收了第二个参数2的地址 并且把返回的值传入给第一个变量a 这个a就变成了2

4:然后通过循环 调用va_arg并且把值加到sumval中

5:循环结束 结束POINT指针

6:返回sumval

                                                                                                                                                                                                                          

在C++中使用换行可以用std::endl(如果加上using namespace std;则不需要加std::)或者是"\n"换行

# include 

int main(void)
{
	int a = 0;
	int b = 20;

	

	std::cout << a;//输出a的值 因为cout是包含在std这个命名空间里的 所以要假设空间名和作用域运算符

	std::cout << a << std::endl;//输出a之后 加上换行
	std::cout << a << "\n";//使用\n换行
	std::cout << a << "hahah" << std::endl;//输出字符串


	return 0;
}

 

两者的区别是:

  • \n只代表换行的转义字符;\n是C中间的格式输出换行,C++保留了下来;输出'\n'是实际输出了的'\10',往输出流里添加了信息,所有的字符都是'\xx'的形式,换行符也是,你用其它任何字符输出一下,前面都会有四个'*'填充的。
  • endl除了代表换行,还紧跟着清出缓冲槽;endl是C++中使用的io流换行;输出endl不会往输出流里添加东西,只会简单的刷新流并换行,但这种换行并没有往流里添加信息,所以那一行不会用'*'来填充

4:缺省参数

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

 

4.1:全缺省参数和半缺省参数(C语言不支持)

 

# include 
using namespace std;

void Fun1(int a = 1, int b = 2, int c = 3)//给abc三个变量全部赋了一个默认值 就叫全缺省参数
{
	cout << a << b << c << endl;
}

void Fun2(int a, int b, int c = 3)//给abc三个变量的部分变量赋了一个默认值 这叫半缺省参数
{
	cout << a << b << c << endl;
}

/*
void Fun2(int a = 2, int b, int c)//这样子不行
void Fun2(int a, int b = 3, int c)//这样子不行
void Fun2(int a = 1, int b = 3, int c)//这样子不行
void Fun2(int a = 1, int b, int c = 4)//这样子不行
半缺省参数的赋值只能是从右往左依次赋值 不能间隔 也不能从左往右(除非全部赋值)
*/

int main(void)
{
	//给全缺省或者半缺省参数传递实参的时候 是从左往右传递
	Fun1(10, 20, 30);//输出10 20 30
	Fun1(10, 20);//输出10 20 3
	Fun1(10);//输出10 2 3
	Fun1( );//输出1 2 3


	return 0;
}

1:全缺省参数即给函数的每一个参数都赋了一个初值

2:半缺省参数是从右往左给函数的部分参数赋了一个初值

3:给全缺省参数或者半缺省参数传递实参的时候 是从左往右赋值的

 

# include 
using namespace std;

void Fun1(int a = 1, int b = 2, int c = 3);//函数的声明中也给了默认值

void Fun1(int a = 1, int b = 2, int c = 3)//函数的定义中给了默认值
{
	cout << a << b << c << endl;
}

int main(void)
{
	Fun1();
	return 0;
}

1:在函数的定义和声明的时候 都给到了默认值 这样是错误的 因为C++会默认考虑到 万一用户在函数的声明和定义中给到的默认值不同 系统无法识别 最终以哪一个为准 所以两者不能同时存在默认值

2:最好在函数的声明中给到

 

# include 
using namespace std;

int hehe = 100;//定义全局变量hehe
int haha = 200;//定义全局变量哈哈

void Fun1(int a = hehe , int b = haha+10, int c = 3)//函数的定义中给了默认值
{
	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
}

int main(void)
{
	Fun1();
	return 0;
}

1:全缺省参数和半缺省参数的默认值可以是全局变量或者是常量 也可以是全局变量和常量的表达式

 

5:函数重载

5.1 函数重载概念

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

也就是说 在同一个作用域中函数的名字必须相同 但是函数的形参(个数 类型 顺序)必须不同 (其中与函数的返回值类型不同没有关系 如果两个参数仅仅是因为返回值类型不同 则不构成重载)记住 一定是同一个作用域中

//情况1参数个数不同
int fun1(int a, int b);
int fun1(int a);
int fun1(void);

//情况2 参数类型类型不同
double fun1(int a, int b);
double fun1(double a, double b);;

//参数顺序不同
int fun1(int a, int b);
int fun1(int b, int a);

//返回值类型不同  这样不行 不构成函数的重载
int fun1(int a, int b);
double fun1(int a, int b);

5.2函数重载的原理:

在编译阶段,编译器会对函数实参类型进行推演,根据推演的结果选择合适的函数进行调用。如果有则调用,如果没有合适类型,编译器尝试进行隐式类型转换,如果转换之后有合适的类型就调用,否则就报错。

函数重载是在编译阶段来根据实参的类型来确定到底调用哪个重载的方法的。

但是编译阶段 他只是找到是否存在该函数的声明 并不会对函数的定义进行查找 所以还有一个点是 如果存在一个函数的声明 但是没有定义 可以过编译阶段 但是过不了链接阶段

 

5.21:那为什么C++支持函数的重载 而C语言不支持呢

因为C语言的编译器在编译时会对函数的名字进行修改, 修改规则:仅仅只是在函数名字前加了一个下划线

_函数名

如下:

 

# include 

void TestFunc(int a, int b);

void TestFunc(int a);


int main(void)
{
	TestFunc(10);
	return 0;
}

 

所以C语言程序在编译的时候 如果出现同名字的函数 在编译的时候就会报错 因为对函数的识别的方法是一样的 都是_TestFunc

而C++支持是因为 C++程序在编译的时候 对于同名但是不同参数的函数 其对函数的认知是不同的

如下

void TestFunc(int a, int b);

void TestFunc(int a);

C++入门_第2张图片

C++入门_第3张图片

5.22:如果让一个C++函数使用C语言的方式进行编译

 

extern "C" void TestFunc(int a);


int main(void)
{
	TestFunc(10);
	return 0;
}

C++完全兼容C,当extern与“C”连用时,作用是告诉编译器用C的编译规则去解析extern “C”后面的内容。最常见的差别就是C++支持函数重载,而标准C是不支持的。

 

5.23:_cdecl 调用约定

cdecl(C declaration,即C声明)是源起C语言的一种调用约定,也是C语言的事实上的标准。在x86架构上,其常用的调用约定内容包括:

1: 函数实参在线程栈上按照从右至左的顺序依次压栈。

2: 函数结果保存在寄存器EAX/AX/AL中

3: 浮点型结果存放在寄存器ST0中

4: 编译后的函数名前缀以一个下划线字符

5: 调用者负责从线程栈中弹出实参(即清栈)

6: 8比特或者16比特长的整形实参提升为32比特长。

7: 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS

8: 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS

9: RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)

 

Visual C++规定函数:

返回值如果是POD值且长度如果不超过32比特,用寄存器EAX传递;

长度在33-64比特范围内,用寄存器EAX:EDX传递;

长度超过64比特或者非POD值,则调用者为函数返回值预先分配一个空间,把该空间的地址作为隐式参数传递给被调函数。

 

GCC的函数返回值都是由调用者分配空间,并把该空间的地址作为隐式参数传递给被调函数,而不使用寄存器EAX。GCC自4.5版本开始,调用函数时,堆栈上的数据必须以16B对齐(之前的版本只需要4B对齐即可)。

 

6:引用

引用概念:

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

6.1:引用的使用1

 

# include 

using namespace std;


int main(void)
{
	int a = 10;
	int& Ra = a;//int& 代表引用变量的类型  这个时候 Ra就是a的引用
	printf("%d\n", a);//输出10
	printf("%d\n", Ra);//输出10
	Ra = 3;//Ra修改了值  相当于a的值也被修改了
	printf("%d\n", a);//输出3
	printf("%d\n", Ra);//输出3
   //int& Rc;//错误 引用变量必须初始化
	//char& Ra = a; //引用的类型必须和被引用的变量类型是一致的

	return 0;
}

对一个变量进行引用,那么引用的变量其实就相当于被引用的变量 两者公用一块空间 所以修改引用变量的值 那么被引用变量的值也会被修改

同时 引用变量的类型必须和被引用变量的类型一致(除非是常引用)

6.2:引用变量的使用2

 

# include 

using namespace std;

int main(void)
{
	int a = 10;
	int b = 12;

	int& Ra = a;//int& 代表引用变量的类型  这个时候 Ra就是a的引用
	int& RRa = a;//RRa也是a的引用
	int& RRRa = RRa;//RRRa是RRa的引用 但是也是a的引用

	printf("a = %d\nRa = %d\nRRa = %d\nRRRa = %d\n", a, Ra, RRa, RRRa);

	//int& Ra = b;//错误 试图想让Ra成为b的引用 但是这样是重复定义

	Ra = b;//但是如果这样 a变量的引用接收了变量b的值 那么a变量的所有引用甚至a变量本身都会变成b变量的值
	printf("a = %d\nRa = %d\nRRa = %d\nRRRa = %d\n", a, Ra, RRa, RRRa);//都是输出12

	return 0;
}

一个变量可以有多个引用,但是一个引用只能作为一个变量的引用,而不能是多个变量的引用

如果一个引用变量接收了一个同类型其他变量的值 那么作为跟该引用共处一块空间的其他引用和变量的值都会被改变

6.3:引用变量的使用3(常引用)

 

# include 

using namespace std;

int main(void)
{
	//int& Ra = 10;//这样是错误的 因为10是一个常量
	const int& Ra = 10;
	//Ra = 20;//这样是错误的 因为Ra是一个常引用 无法别改变值
	//printf("%d\n", Ra);

	const int a = 10;//定义了一个常变量a
	//int& RRa = a;//这样是错误的 因为a是一个常变量
	const int& RRa = a;
	//RRa = 20;//这样是错误的 因为RRa作为一个常变量的引用 变量变量的值不能被修改 那么就不能通过变量的引用修改变量的值


	double b = 10.1;
	const int& RRRa = b;
	printf("%d\n%lf\n", RRRa, RRRa);//输出 10   0.000000 

	return 0;
}

1:一个引用如果想要接收一个常量的值 那么该引用就要定义成一个常引用

2:一个常引用作为一个常变量的引用 那么就无法通过该引用从而去修改常变量的值 因为常变量本身的值是无法被修改的 那么理所当然就无法通过该引用去修改

3:为什么一个整型的常引用却可以接收一个双精度浮点型的变量b 不是说引用的类型和被引用的类型要一致吗

其实 这里并不是RRRa作为变量b的引用 而是浮点型变量创建了一个临时变量 这个临时变量里存放了整型变量10 相当于RRRa是作为一个临时变量的引用 如下所示 b的地址和RRRa的地址 表明两者不是公用一块空间的

C++入门_第4张图片

6.4:引用变量的使用4

 

# include 

using namespace std;

void Swap(const int& a, const int& b);//如果形参是常引用 那么就不能改变实参的值

void Swap(int& a, int& b)//使用引用作为形参接收实参的值
{
	//修改引用就可以修改实参的值
	int tmp = 0;
	tmp = a;
	a = b;
	b = tmp;
}


int main(void)
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d  b = %d\n", a, b);
	Swap(a, b);
	printf("交换后:a = %d  b = %d\n", a, b);

	return 0;
}

在C++中使用引用也是可以达到不用传递指针从而能修改实参的值

但是如果不希望因为形参是引用而修改实参的值的话 那么就把形参定义成常引用

6.5:引用变量的使用5

 

# include 

using namespace std;

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}

int main(void)
{
	int &i = Add(1, 2);
	printf("%d\n", i);//输出3
	printf("%d\n", i);//输出随机值
	printf("%d\n", i);//输出随机值

	return 0;
}

函数Add作为的返回值是一个整型引用的类型 在main函数中 使用引用i接收了返回来的值,但是发现输出的时候 i的值只有第一次输出是3 后面几次都是随机值 为什么?

当执行第一个printf函数的时候

当执行第二个printf函数的时候

C++入门_第5张图片

当执行第三个printf函数的时候

C++入门_第6张图片

可以看到 在调用完Add函数之后 这个时候 引用i指向了所开辟的栈空间的位置 并且取到了里面的i的值 但是执行第二个printf的时候 i仍旧是指向那个空间的 但是因为内存被系统回收了 所以里面的值是随机值 第三个也是一样的道理

 

6.6:形参为传值、传地址、传引用的三者比较

 

# include 
# include 

using namespace std;

struct Test
{
	int a[10000];
};

void Fun1(struct Test a){}//传值
void Fun2(struct Test* pa){}//传地址
void Fun3(struct Test& Ra){}//传引用

void TestFast(int N)
{
	struct Test a;
	size_t Begin1 = clock();
	for (int i = 0; i < N; i++)
	{
		Fun1(a);
	}
	rsize_t End1 = clock();
	printf("传值的速度是:%d\n", End1 - Begin1);//输出66

	size_t Begin2 = clock();
	for (int i = 0; i < N; i++)
	{
		Fun2(&a);
	}
	rsize_t End2 = clock();
	printf("传址的速度是:%d\n", End2 - Begin2);//输出2

	size_t Begin3 = clock();
	for (int i = 0; i < N; i++)
	{
		Fun3(a);
	}
	rsize_t End3 = clock();
	printf("传引用的速度是:%d\n", End3 - Begin3);//输出2
}

int main(void)
{
	TestFast(100000);

	return 0;
}

可以发现 传地址和传引用的效率是差不多的 都远远比传值来的高的多

但是指针和引用又有什么区别呢?

相同点:

1:引用和指针的效率相近

2:引用和指针在底层实现上是一样的:在底层是实现上,引用实际上就是按照指针的方式实现的,即引用在底层实际上也是有空间的(不是和被引用的变量共处的一块空间,而是像指针一样有自己的独立空间)

如下代码

 

# include 

using namespace std;

int main(void)
{
	int a = 10;
	int* p = &a;
	int& Ra = a;
}

指针变量p和引用Ra的实现在底层实现上是一致的,分别都是取到a的地址 然后把地址传递给相应的变量,但是跟指针有些许不同的是 引用虽然在底层也是使用指针实现的 但是类型确实 类型*const变量名 使用const修饰解引用前的变量 意思就是 指向是不可改变的 但是可以修改值 这也是刚好符合引用的特点

C++入门_第7张图片

不同点:

1:引用必须要初始化,但是指针可以不用初始化(野指针)

2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

3. 没有NULL引用,但有NULL指针

4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)

 

int main(void)
{
	double a = 10.11;
	double* p = &a;
	double&Ra = a;
	printf("指针所占字节大小:%d\n", sizeof(p));//输出4
	printf("引用所占字节大小:%d\n", sizeof(Ra));//输出8
}

5. 引用自增即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

int main(void)
{
	double a = 10.11;
	double* p = &a;
	double& Ra = a;
	//p++;//这样是没有意义的 因为p指针向后移动8个字节的空间 但是指向哪里未知
	Ra++;
	printf("%lf\n", Ra);//输出11.110000
}

6. 有多级指针,但是没有多级引用

 

int main(void)
{
	int a = 10;
	int* p = &a;//一级指针
	int** pt = &p;//二级指针

	int& Ra = a;
	int&& Raa = a;//这样是错误的
}

7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理

8. 引用比指针使用起来相对更安全

 

 

7:内联函数

7.1 概念

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

# include 

using namespace std;

void Add(int left, int right)
{
	cout << left + right << endl;
}

int main(void)
{
   int ret = 0;
	ret = Add(1, 2);
}

 

没有加inline修饰的函数 会在反汇编中有这么一句 call 函数名 即对函数的调用

C++入门_第8张图片

# include 

using namespace std;

inline void Add(int left, int right)
{
	cout << left + right << endl;
}

int main(void)
{
	int ret = 0;
	ret = Add(1, 2);
}

加上inline修饰之后的修函,在反汇编中是这样的, 少了函数调用反之用函数体替换函数调用

C++入门_第9张图片

7.2 特性

1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。

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

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

 

7.3:C++有哪些技术替代宏?

1. 常量定义换用const

2. 函数定义换用内联函数(直接就用函数体替换函数调用)

附加:什么是宏,宏的优点和缺点, 以及offsetof宏函数的实现和模拟

1:宏就是 #define定义的内容

2:宏的优点

宏可以完成一些函数完不成的事情

宏常量可以是代码的效率变高,如#define MAX 100 修改一个 整个代码MAX就修改了

宏函数因为相似内联函数 所以在部分函数效率上是高于正常函数的

3:宏的缺点

不方便调试宏。(因为预编译阶段进行了替换)

导致代码可读性差,可维护性差,容易误用。

没有类型安全的检查 。

4:offsetof宏函数的实现和模拟

函数原型:offsetof (type,member)

此宏以函数形式返回数据结构或联合类型中成员成员的偏移值(以字节为单位)。

返回的值是size_t类型的无符号整型值,包含指定成员与其结构开头之间的字节数

函数的实现:

 

# include 
# include 

struct Student
{
	char name[3];
	int age;
	float score;
};

int main(void)
{
	struct Student S1;
	printf("%d\n", offsetof(struct Student, name));//相对结构体首地址偏移量0
	printf("%d\n", offsetof(struct Student, age));//相对结构体首地址偏移量4
	printf("%d\n", offsetof(struct Student, score));//相对结构体首地址偏题量8


	return 0;
}

证明:

C++入门_第10张图片

# include 
# include 

# define MY_OFFSETOF(type, membe)  (int)(&(((type*)0)->membe))
/*
因为结构体首个成员变量的地址是从相对结构体首地址偏移量为0的地址开始的
所以 我们假设把0作为结构体的地址 
然后取到结构体指针所指向的成员变量的值 取到他的位置 就可以知道 相当于起始位置 
他当前的位置是多少 然后强制转换成整型 相当于该地址减去了起始地址的偏移量
*/
struct Student
{
	char name[3];
	int age;
	float score;
};

int main(void)
{
	struct Student S1 = {0};
	printf("%d\n", MY_OFFSETOF(struct Student, name));//相对结构体首地址偏移量0
	printf("%d\n", MY_OFFSETOF(struct Student, age));//相对结构体首地址偏移量4
	printf("%d\n", MY_OFFSETOF(struct Student, score));//相对结构体首地址偏题量8


	return 0;
}

8:auto关键字

8.1 auto简介

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,

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

# include 

using namespace std;

int main(void)
{
	int a = 10;
	double b = 10.111;
	auto c = a;
	auto d = b;
   //auto e;这样是错误的 因为系统无法识别e的类型
	cout << typeid(c).name() << endl;//输出int
	cout << typeid(d).name() << endl;//输出double

	return 0;
}

 

在编译期间,系统会根据表达式的类型,会给auto后面的的变量推导相同的类型,但是如果没有初始化,那么系统是无法识别变量的类型的 这样是错误的

因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。

 

void Fun1(auto a)
{}
void Fun1(auto a = 10)
{}

作为函数的形参也是不行的,即使是作为全缺省参数 也是不可以的 因为当遇到不带有默认值的形参的时候 编译器在编译阶段是识别不了a这个变量的类型的 所以即使可以带有默认值,但不是所有的形参都带有默认值的 所以编译器默认不背锅 不能使用auto作为函数的形参

8.2 auto的使用1(指针和引用)

 

# include 

using namespace std;

int main(void)
{
	int a = 10;
	//这两者是没有任何区别的
	auto p1 = &a;
	auto* p2 = &a;

	auto& Ra = a;//作为引用的话 必须要加上& 不然系统只能识别成int
    cout << typeid(p1).name() << endl;//输出int*
	cout << typeid(p2).name() << endl;//输出int*
	cout << typeid(Ra).name() << endl;//输出int
	return 0;
}

auto用于指针的时候,可以给出*号 也可以不使用*号 因为系统根据后面的表达式,知道传递的是a的地址

但是用于引用的时候,必须要给出&号 因为如果不给出 系统根据后面的表达式,会觉得是int类型,这样就成了定义了一个整型变量 而不是引用

8.2 auto的使用2(同一行定义多个变量)

 

# include 

using namespace std;

int main(void)
{
	auto a = 1, b = 2;
	//auto c = 3, d = 10.1;这样是错误的
	//因为系统在识别后面的表达式的时候 会产生矛盾 因为一个是int 一个是double
   cout << typeid(a).name() << endl;//输出int
	cout << typeid(b).name() << endl;//输出int

	return 0;
}

8.3 auto的使用3(是否能用于数组)

 

# include 

using namespace std;

int main(void)
{
	int a[3] = { 1, 2, 3 };
	//auto b[3] = { 1, 2, 3 };//这样是错误的
	//因为系统不知道要给b带去int类型还是数组的类型
	cout << typeid(a).name() << endl;//输出a的类型 int [3]

	return 0;
}

9:for循环(有范围的循环)

 

# include 

using namespace std;

void Print(int a[])//这样是不行的 因为无法确定a的范围
{
    for(auto e2 : a)
}       cout<< e2 <

for循环后的括号由冒号“ :”分为两部分:

第一部分是范围内用于迭代的变量

第二部分则表示被迭代的范围。

9.1 范围for的使用条件

1. for循环迭代的范围必须是确定的 对于数组而言,就是数组中第一个元素和最后一个元素的范围;

2. 迭代的对象要实现++和==的操作。

 

10:空指针nullptr(C++11)

情况1:在C++98中的空指针

 

int main(void)
{
	int* p = NULL;

	return 0;
}

但是这样的指针是存在一定的问题的 如下代码

void Fun(int a)
{
	cout << "haha" << endl;
}

void Fun(int* a)
{
	cout << "hehe" << endl;
}

int main(void)
{
	Fun(NULL);//输出结果haha

	return 0;
}

发现输出的结果是haha 但是不应该是输出hehe吗 这个就是NULL这个空指针的弊端所在

因为NULL实际是一个宏,在头文件stddef.h中,可以看到NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量

所以在C++中我们对于空指针 一把都是使用nullprt 如下代码

# include 

using namespace std;

void Fun(int a)
{
	cout << "haha" << endl;
}

void Fun(int* a)
{
	cout << "hehe" << endl;
}

int main(void)
{
	Fun(nullptr);//输出hehe
	int* p = nullptr;

	cout << "指针p的大小" << sizeof(p) << endl;//输出4
	cout << "空指针的大小" << sizeof(nullptr) << endl;//输出4

	return 0;
}

 

 

你可能感兴趣的:(自用复习,C++,c++,编程语言)