0
点赞
收藏
分享

微信扫一扫

湘潭大学软件工程算法设计与分析考试复习笔记(二)

前面的博客已经学习了C语言以及使用C语言来实现一些数据结构~这一篇博客我们将启航C++,去领略更加深层次计算机语言的魅力~准备好了吗?我们发车了~

目录

C++发展历史

C++版本更新

C++的第一个程序

命名空间

namespace的价值

namespace的定义及注意点

namespace的使用

C++输入&输出

 缺省参数

函数重载

引用

引用的概念和定义

引用的特性

 引用的使用

const引用

指针和引用的关系

nullptr

inline


C++发展历史

      C++的起源可以追溯到1979年,当时Bjarne Stroustrup(本贾尼·斯特劳斯特卢普(C++祖师爷)(这里翻译的名字不同的地方可能有差异) 在贝尔实验室从事计算机科学和软件工程的研究工作。面对项目中复杂的软件开 发任务,特别是模拟和操作系统的开发工作,他感受到了现有语言(如C语言)在表达能力、可维护性 和可扩展性方面的不足。

      1983年,Bjarne Stroustrup在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形, 这个时候C++已经有了类、封装、继承等核心概念,为后来的面向对象编程奠定了基础。这⼀年该语言被 正式命名为C++。 (不得不说,祖师爷就是祖师爷,直接自己创建了一个新的计算机语言)

        在随后的几年中,C++在学术界和工业界的应用也就逐渐增多。一些大学和研究所开始将C++作为教学和研 究的首选语言,而⼀些公司也开始在产品开发中尝试使用C++。这⼀时期,C++的标准库和模板等特性 也得到了进⼀步的完善和发展。

        C++的标准化工作于1989年开始,并成立了⼀个ANSI和ISO(International Standards

Organization)国际标准化组织的联合标准化委员会。1994年标准化委员会提出了第⼀个标准化草

案。在该草案中,委员会在保持斯特劳斯特卢普最初定义的所有特征的同时,还增加了部分新特征。在完成C++标准化的第⼀个草案后不久,STL(Standard Template Library)是惠普实验室开发的⼀系 列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室⼯作时所开发 出来的。在通过了标准化第⼀个草案之后,联合标准化委员会投票并通过了将STL包含到C++标准中的 提议。STL对C++的扩展超出C++的最初定义范围。虽然在标准中增加STL是个很重要的决定,但也因 此延缓了C++标准化的进程。

        1997年11月14日,联合标准化委员会通过了该标准的最终草案。1998年,C++的ANSI/IS0标准被投入使用~后来经历了多次重要的版本更新和标准修订,这些标准和修订不断扩展和完善了C++的功能和性能,使其在软件开发中得到了更广泛的应用。

放上一张祖师爷照片~

C++版本更新

C++经过几十年的发展,不断地在更新完善~

可以看看下面的图片来了解一下C++的版本更新~

这里呢~加粗的就是改动比较大的版本~

可以看出来现在的C++基本上三年会有一次更新和修订~这些标准和修订不断扩展和完善了C++的功能和性能,使其在软件开发中得到了更广泛的应用~

已经说了C++的发展历史和它的版本更新,接下来我们就开始C++这一门计算机语言~

C++的第一个程序

以前我们C语言阶段使用VS创建源文件的时候,是以.c为后缀的源文件,这里我们需要使用C++的语法和库的话就需要使用.cpp为后缀.这样的话,VS编译器看到是.cpp就会调用C++编译器编译,linux下要用g++编译,不再是gcc。

前面我们学到C语言第一个程序打印hello world,事实上,C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以在当前文件下运行。

C语言版:

//C语言版
#include<stdio.h>
int main()
{
	printf("hello world\n");
	return 0;
}

但是C++有⼀套自己的输⼊输出,严格说C++版本的hello world应该是这样写。

C++版:

//C++版
#include<iostream>
using namespace std;
int main()
{
	cout << "hello world\n" << endl;
	return 0;
}

这一段C++代码可能看着有点懵逼,没关系,接下来跟我一起开启C++的美妙之旅~

命名空间

命名空间也叫名字空间,我们会使用namespace关键字

namespace的价值

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

我们来看看下面C语言版本命名冲突的例子:

//命名冲突
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
	// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”
	printf("%d\n", rand);
	return 0;
}

我们可以看到编译报错,这是因为在在stdlib头文件中,包含了rand这样一个函数,在预编译阶段会把这个头文件进行展开,那么在全局域这个范围内就有两个rand,那么也就造成了rand重复定义,进而报错~

如果我们注释掉#include <stdlib.h>就不会出现这个问题~

这个时候我们就需要需要使用namespace来解决这个问题了~

namespace的定义及注意点

例:

namespace xiaodu
{
	//定义变量
	int rand = 10;

	//定义函数
	int Mul(int a, int b)
	{
		return a * b;
	}

	//定义类型
	struct Node
	{
		int data;
		struct Node* next;
	};//结构体后面有分号
}//这里没有分号

这里需要特别注意的是使用namespace来定义一个域,{ }后面是没有分号的,与我们前面学习的结构体进行一个区分~

int main()
{
	//这里访问全局的rand也就是一个函数,应该打印出来一个地址
	printf("%d\n", rand);
	printf("%p\n", rand);
	return 0;
}

例:



#include<stdio.h>
#include<stdlib.h>
namespace xiaodu
{
	//定义变量
	int rand = 10;

	//定义函数
	int Mul(int a, int b)
	{
		return a * b;
	}

	//定义类型
	struct Node
	{
		int data;
		struct Node* next;
	};//结构体后面有分号
}//这里没有分号

int main()
{
	//这里访问全局的rand也就是一个函数,应该打印出来一个地址
	printf("%p\n", rand);
	//访问域里面的rand
	printf("xiaodu::rand = %d\n", xiaodu::rand);
	//访问域里面的Mul函数
	printf("xiaodu::Mul(2,4) = %d\n", xiaodu::Mul(2, 4));
	return 0;
}

我们可以看出namespace是不可以定义在局部的,但是可以嵌套定义~就像有多个公司,每一个公司内部又会有不同的部门~

例:


#include<stdio.h>
#include<stdlib.h>

namespace B
{
	//namespace可以嵌套定义
	namespace data
	{
		int rand = 10;

		int Mul(int a, int b)
		{
			return a * b;
		}
	}
	//不同的域可以定义同名变量
	namespace st
	{
		int rand = 1;

		struct Node
		{
			int data;
			struct Node* next;
		};
	}
}//这里没有分号

int main()
{
	printf("%p\n", rand);//打印地址,全局域的函数rand
	printf("B :: data :: rand = %d\n", B::data::rand);
	printf("B :: st :: rand = %d\n", B::st::rand);
	return 0;
}

例:(以栈为例)

Stack.h

Stack.cpp

test.cpp


#include"Stack.h"
// 全局定义了⼀份单独的Stack
typedef struct Stack
{
	int a[10];
	int top;
}ST;
void STInit(ST* ps) {}
void STPush(ST* ps, int x) {}
int main()
{
	// 调用全局的
	ST st1;
	STInit(&st1);
	STPush(&st1, 1);
	STPush(&st1, 2);
	printf("sizeof(st1) = %d\n", sizeof(st1));
	// 调用命名空间域的
	MyStack::Stack st2;
	printf("sizeof(st2) = %d\n", sizeof(st2));
	MyStack::StackInit(&st2);
	MyStack::StackPush(&st2, 3);
	MyStack::StackPush(&st2, 4);
	return 0;

结合结构体部分的知识,我们也就可以知道这两个的大小~不清楚的可以看看这一篇博客~C语言——自定义类型

结合上面的测试,我们可以看出 编译器会自动将不同文件中同名的namespace合并在一起,不会发生冲突~

namespace的使用

所以下面程序会编译报错

#include<stdio.h>
namespace xiaodu
{
	int a = 10;
	int b = 100;
}
int main()
{
	// 编译报错:error C2065: “a”: 未声明的标识符
	printf("%d\n", a);
	return 0;
}

我们要使用命名空间中定义的变量/函数,有三种方式:

例:


#include<stdio.h>
namespace xiaodu
{
	int a = 10;
	int b = 100;
}
int main()
{
	printf("xiaodu::a = %d\n", xiaodu::a);
	printf("xiaodu::b = %d\n", xiaodu::b);
	return 0;
}

但是我们要使用很多次命名空间域里面的变量或者函数时,每一次都加上命名空间域名就会很麻烦~所以就有了第二个方法

例:


#include"Stack.h"
//使用using展开命名空间中全部成员
using namespace MyStack;
int main()
{
	Stack st;
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	StackPush(&st, 4);
	StackPush(&st, 5);
	StackPush(&st, 6);
	StackPush(&st, 7);
	printf("StackTop = %d\n", StackTop(&st));
	StackDestory(&st);
	return 0;
}

这种方法也有缺陷,相当于原来封装好的域又给他展开了,所以在项目中不推荐~来看看下一个方式~

例:


#include"Stack.h"
//使用using展开部分命名空间中成员
//展开StackPush
//这里不需要关键字namespace(展开部分命名空间中成员)
using MyStack::StackPush;
int main()
{
	MyStack::Stack st;
	MyStack::StackInit(&st);
	//StackPush经常使用
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	StackPush(&st, 4);
	StackPush(&st, 5);
	StackPush(&st, 6);
	StackPush(&st, 7);
	printf("StackTop = %d\n", MyStack::StackTop(&st));
	MyStack::StackDestory(&st);
	return 0;
}

这三种方式,我们可以根据自己实际需要进行使用~

C++输入&输出

我们可以在Cplusplus网站上找到它~

说了这么多,是不是还是更加云里雾里~我们结合下面的例子来看看~

例:


//标准的输入、输出流库
#include<iostream>
//不包含<stdio.h>,因为vs系列编译器<iostream>间接包含它

//C++标准库都放在⼀个叫std(standard)的命名空间中
//使用using进行展开
using namespace std;

int main()
{
	
	int a = 0;
	double b = 0;
	//C++的输入输出可以自动识别变量类型
	//同时可以连续输入输出
	//cout ——输出, cin——输入
	cout << "enter a、b:";
	cin >> a >> b;
	//std::endl 是一个函数,流插入输出时,相当于插入⼀个换行字符加刷新缓冲区
	cout << "a = " << a << '\n' << "b = " << b << endl;
	return 0;
}

这样看起来,C++的输入输出比我们的C语言简单多了,有没有体会到C++的魅力呢?

 缺省参数

什么是缺省参数呢?看这个名字,我们可以知道它是一个参数~我们一起来看看它的定义和使用~

知道了概念,来看看下面代码的简单使用~

#include<iostream>
using namespace std;
//缺省参数
void func(int a = 10)
{
	cout << "a = " << a << endl << endl;
}
int main()
{
	func();//没有实参,使用缺省参数10
	func(100);//有实参,使用实参100
	return 0;
}


#include<iostream>
using namespace std;
//缺省参数

// 全缺省
void Func1(int a = 1, int b = 2, int c = 3)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

//半缺省(部分缺省)
void Func2(int a, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
int main()
{
	// 全缺省调用
	Func1();//不传参,为默认缺省值
	Func1(11);//传一个参,第一个为传的实参值,其他的为默认缺省值
	Func1(11,22);//传两个参,前面两个为传的实参值,其他的为默认缺省值
	Func1(11,22,33);//传三个参,都为传的实参值

	// 半缺省调用
	//Func2两个参数是缺省参数,至少传一个参数
	Func2(100);//传一个参,第一个为传的实参值,其他的为默认缺省值
	Func2(100, 200);//传两个参,前面两个为传的实参值,其他的为默认缺省值
	Func2(100, 200, 300);//传三个参,都为传的实参值
	return 0;
}

像我们的顺序表初始化的时候,就可以给定一个大小n,创建相应大小的空间~这样也就可以提高效率~这就是我们缺省参数的妙处所在~但是使用缺省参数需要注意下面几个点

比如下面的代码就会报错:

void Func(int a = 10,int b,int c = 30)//err
//C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

比如下面的代码就会报错:

void Func(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
int main()
{
	Func();
	Func(1, , 3);//err
	//想直接给第一个和第三个传参是找不到的
	//带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。 
	return 0;
}

比如下面的代码就会报错:

#include<iostream>
using namespace std;

//函数声明
void Func(int a = 10, int b = 20, int c = 30);
int main()
{
	Func(1, 2);
	return 0;
}
//函数定义
//err
//函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值
void Func(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

正确代码:函数声明的地方给缺省值

#include<iostream>
using namespace std;

//函数声明
//函数声明给缺省值
void Func(int a = 10, int b = 20, int c = 30);
int main()
{
	Func(1, 2);
	return 0;
}
//函数定义
void Func(int a, int b, int c)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

函数重载

例:


#include<iostream>
using namespace std;

//1.形参参数类型不同
int Add(int a, int b)
{
	cout << "int Add(int a, int b)" << endl;
	return a + b;
}

double Add(double a, double b)
{
	cout << "double Add(double a, double b)" << endl;
	return a + b;
}
//2.形参参数个数不同
void func(int a)
{
	cout << "void func(int a)" << endl;
}

void func(int a, int b)
{
	cout << "void func(int a, int b)" << endl;
}
int main()
{
	//形参参数类型不同测试
	Add(1, 2);
	Add(1.1, 2.2);
	//形参参数个数不同测试
	func(1);
	func(1, 2);
	return 0;
}

前面的使用没有问题,我们来看看下面的代码~

#include<iostream>
using namespace std;

void f()
{
	cout << "f()" << endl;
}
void f(int a = 10)
{
	cout << "f(int a = 10)" << endl;
}
int main()
{
	f();
	return 0;
}

上面的f是函数重载吗?它的输入结果是什么?

如果给了一个实参,那么就会调用第二个函数~

引用

接下来就是重头戏了~引用~

引用的概念和定义

#include<iostream>
using namespace std;
int main()
{
	int a = 1;
	// 引用:b和c是a的别名
	int& b = a;
	int& c = a;
	++c;
	// 同时也可以给别名b取别名,d相当于还是a的别名
	int& d = b;
	++d;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << "d = " << d << endl;
	// 取地址我们看到是⼀样的
	//已存在变量和它引用的变量共用同一块内存空间
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;
	return 0;
}

所以,引用就是给已存在变量取一个别名,已存在变量和它引用的变量共用同一块内存空间

上面的代码中a、b、c、d共用同一块内存空间

引用的特性

例:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	//int& ra;
	// 编译报错:“ra”: 必须初始化引用
	//正确引用
	int& b = a;
	//a,b共用一块内存空间
	int c = 20;
	b = c;
	// 不是让b引用c,因为C++引用不能改变指向
	// 这里是正常的赋值
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	return 0;
}

代码结果:

 引用的使用

引用传参:


#include<iostream>
using namespace std;
//引用传参
//x,y是a,b的别名
void Swap1(int& x, int& y)
{
	int tmp = x;
	x = y;;
	y = tmp;
}
//指针版本
void Swap2(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap1(a, b);
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	int c = 1;
	int d = 2;
	Swap2(&c, &d);
	cout << "c = " << c << endl;
	cout << "d = " << d << endl;
	return 0;
}

知道了引用,我们来看看下面有趣的代码


#include<iostream>
using namespace std;

typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode, * PNode;

//LTNode就是结构体,这里PNode也就是一个指针变量——LTNode*类型
// 指针变量也可以取别名,LTNode*& phead就是给指针变量取别名
// 这样就不需要使用二级指针,简化了程序
// 转换过程
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
	PNode newnode = (PNode)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	newnode->val = x;
	newnode->next = NULL;
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		//...
	}
}

int main()
{
	PNode plist = NULL;
	ListPushBack(plist, 1);
	return 0;
}

这样使用引用也就方便了很多,这也就是引用的妙处所在~

const引用

例:

#include<iostream>
using namespace std;

int main()
{
	const int a = 10;
	//int& ra = a;
	// 编译报错:error C2440: “初始化”: 无法从“const int”转换为“int &”
	// 这里引用是对a访问权限的放大
	
	//正确写法:权限平移(引用⼀个const对象,必须用const引用)
	const int& ra = a;
	//a++;
	// 编译报错:error  C3892: “a”: 不能给常量赋值
	//ra++;
	// 编译报错:error C3892: “ra”: 不能给常量赋值

	//正常的赋值拷贝,不涉及到权限的放大缩小
	int rra = a;
	rra++;
	
	cout << "a = " << a << endl;
	cout << "ra = " << ra << endl;
	cout << "rra = " << rra << endl;

	int b = 20;
	const int& rb = b;
	// 这里引用是对b访问权限的缩小
	// 编译报错:error C3892: “rb”: 不能给常量赋值
	//rb++;//rb const修饰
	b++;//正常,b没有被const修饰
	cout << "b = " << b << endl;
	cout << "rb = " << rb << endl;
	return 0;
}

例:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& ra = 30;
	// 编译报错: “初始化”: 无法从“int”转换为“int &”
	//int& rb = a * 3;   //(a*3)结果保存在⼀个临时对象中,临时对象具有常性(不能被修改)
	//正确使用:const修饰
	const int& rb = a * 3;
	double d = 12.34;
	// 编译报错:“初始化”: 无法从“double”转换为“int &”
	//int& rd = d; //类型转换产生临时对象,临时对象具有常性(不能被修改)
	//正确使用:const修饰
	const int& rd = d;
	//不进行类型转换
	double& rdd = d;
	return 0;
}

指针和引用的关系

我们来看看下面代码的反汇编:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int& ra = a;

	int b = 100;
	int* rb = &b;
	return 0;
}

我们可以看到指针和引用汇编层是一样的,所以汇编指令层并没有引用的概念,上层引用的语法,在底层/汇编层依然是指针实现的~

nullptr

在C语言阶段,我们认为空指针是NULL,但是NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:

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

例:

#include<iostream>
using namespace std;
void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(0);
	//调用f(int x)
	f(NULL);
	//我们认为NULL是空指针
	// 本想通过f(NULL)调用指针版本的f(int*)函数
	// 但是由于在C++中,NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。
	
    //f((void*)NULL);
	// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型

	//正确调用指针版本:
	f((int*)NULL);//强制类型转换
    f(nullptr);//使用特殊关键字nullptr,nullptr可以转换成任意其他类型的指针类型
	return 0;
}

inline

#include<iostream>
using namespace std;
//使用宏定义一个函数
#define Add1(a,b) ((a)+(b))
//不加分号!!避免空语句和编译报错!!
//使用宏定义不要舍不得括号,考虑运算符优先级问题

inline int Add2(int a, int b)
{
	return a + b;
}
int main()
{
	//1.使用宏函数
	int ret1 = Add1(2, 3);
	cout << "a + b = " << ret1 << endl;
	//预处理进行宏替换
	//cout << "a + b = " << ((2)+(3)) << endl;

	//2.使用inline函数
	int ret2 = Add2(2, 3);
	cout << "a + b = " << ret2 << endl;
	return 0;
}

1.我们进行了如果修改,那么反汇编就是下面的样子,没有call Add2语句,就是展开了inline函数~

2.不进行修改,有call Add2语句,就是没有展开了inline函数,去调用了Add2

例:

// 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()
{
	// 链接错误:无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)
	f(100);
	return 0;
}

正确使用:在头文件里面声明定义一起写

// F.h
#include <iostream>
using namespace std;

inline void f(int i)
{
	cout << i << endl;
}

例:

正确使用:使用 static 修饰普通函数

举报

相关推荐

0 条评论