C++入门

C++教学总目录点此

1、命名空间

命名空间是用来解决命名冲突的,在一些大型的项目中难免会出现命名冲突的问题,C语言就无法解决,C++的命名空间可以很好的解决命名冲突的问题。接下来我们看一个例子:

#include <iostream>
#include <stdlib.h>
using namespace std;

int rand = 0;
int main()
{
	return 0;
}

当进行编译后会发现报错了:
在这里插入图片描述
这里显示的是rand重定义了,之所以报错是因为stdlib.h库里有一个rand的函数,所以导致了命名冲突。那该怎么解决呢?这里就引入C++的命名空间了:

#include <iostream>
#include <stdlib.h>
using namespace std;

namespace zzy
{
	int rand = 0;
}
int main()
{
	return 0;
}

像这样定义了一个zzy的命名空间,这样就不会和库里的rand函数冲突了。

而当我们想访问命名空间的变量时,我们需要通过域作用限定符::来访问

#include <iostream>
#include <stdlib.h>
using namespace std;

namespace zzy
{
	int rand = 0;
}
int main()
{
	int rand = 1;
	printf("%d\n", rand);      // 优先访问局部域  -> 1
	printf("%p\n", ::rand);    // 访问全局域     -> rand函数的地址
	printf("%d\n", zzy::rand); // 访问命名空间域zzy  -> 0
	return 0;
}

在这里插入图片描述

另外命名空间还可以定义结构体、函数,也可以嵌套命名空间,两个同名的命名空间会合并:

#include <iostream>
#include <stdlib.h>
using namespace std;

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

	int add(int x, int y)
	{
		return x + y;
	}
	namespace test
	{
		int rand = 2;
	}
}
namespace zzy
{
	int rand = 1;
}
int main()
{
	printf("%d\n", zzy::rand);       // 输出1
	printf("%d\n", zzy::test::rand); // 输出2
	return 0;
}

我们也可以将命名空间展开到全局,这样就不需要通过域作用限定符访问了,但是要注意不要与全局的其他变量冲突:

#include <iostream>
#include <stdlib.h>
namespace zzy
{
	int x = 1;
	int y = 2;
}
using namespace zzy;
int main()
{
	printf("%d %d", x, y);  // 输出1 2
	return 0;
}

也可以部分展开:

#include <iostream>
#include <stdlib.h>
namespace zzy
{
	int x = 1;
	int y = 2;
}
using zzy::x;  // 只展开了x,访问y还是要通过域作用限定符来访问。
int main()
{
	printf("%d %d", x, zzy::y);  // 输出1 2
	return 0;
}

C++库的实现定义在名为std的命名空间中,当我们要使用C++标准库的方法或者函数时就需要通过域作用限定符去访问,而我们平时一般不需要是因为我们写的代码都带有using namespace std; 相当于把std命名空间展开了。 当然我们也可以展开一部分。

#include <iostream>
#include <stdlib.h>
namespace zzy
{
	int x = 1;
	int y = 2;
}
using namespace zzy;
//using namespace std; 全部展开
// 部分展开
using std::cout; 
using std::endl;
int main()
{
	std::cout << x << " " << y << std::endl; // 不展开,通过域作用限定符访问
	cout << x << " " << y << endl; // 展开到全局,直接使用。
	return 0;
}

在我们做项目的时候一般推荐部分展开,把常用的展开,而我们自己平时写代码为了方便可以全部展开。

2、C++输入&输出

cout是C++的标准输出对象,cin是标准输入对象,使用cout和cin要包含头文件iostream。
endl是C++的特殊符号表示换行输出。 <<是流插入运算符,>>是流提取运算符。 使用cout和cin不需要像C语言printf和scanf指明数据类型。

在这里插入图片描述
而C++控制输出格式比较复杂,所以当我们需要控制输出格式时直接用printf即可。

3、缺省参数

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

可以像下面Func函数给a一个缺省值:

#include <iostream>
#include <stdlib.h>
using namespace std;

void Func(int a = 0)
{
	cout << a << endl;
}
int main()
{
	Func(1); // 显示传参,输出1
	Func();  // 使用默认缺省值,输出0
	return 0;
}

3.1、全缺省

#include <iostream>
#include <stdlib.h>
using namespace std;

void Func(int a = 0, int b = 1, int c = 2)
{
	cout << a << " " << b << " " << c << endl;
}
int main()
{
	Func(1); // 显示传参,输出1 1 2
	Func();  // 使用默认缺省值,输出0 1 2
	return 0;
}

3.2、半缺省

也可以缺省一部分,称为半缺省,注意半缺省必须从右向左缺省,因为我们给函数传参是从左往右的:

#include <iostream>
#include <stdlib.h>
using namespace std;

void Func(int a, int b = 1, int c = 2)
{
	cout << a << " " << b << " " << c << endl;
}
int main()
{
	Func(0); // 显示传参,输出0 1 2
	return 0;
}

3.3、缺省参数的周边问题

另外缺省参数不能在声明和定义中同时出现,例如下面这段代码,在Stack.hpp中声明Stack的初始化函数,在Stack.cpp中定义初始化函数:
在这里插入图片描述
在这里插入图片描述
这样是编不过的,因为如果声明和定义提供的值不同,那么编译器就无法确定要使用哪个缺省值,如果只在定义中出现也是不行的,所以我们只能在声明中给出缺省值。

缺省值的用途:当我们定义一个栈结构出来,如果我们知道要用多少的容量,我们可以直接显示传参去初始化,减少后面扩容的消耗。而如果我们不知道需要多少容量我们就用缺省值。

#include <iostream>
#include <stdlib.h>
using namespace std;

struct Stack
{
	int* a;
	int top;
	int capacity;
};
void StackInit(struct Stack* st, int capacity = 4)
{
	st->a = (int*)malloc(sizeof(int) * capacity);
	st->top= 0;
	st->capacity = capacity;
}
int main()
{
	Stack st1, st2;
	StackInit(&st1, 100); // 知道要用多少数据,就显示传参
	StackInit(&st2);      // 不知道要用多少数据,就用默认的缺省值
	return 0;
}

4、函数重载

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

例如我们要实现一个加法函数,参数既可以是int类型,也可以是double,函数重载可以让我们定义出同名但参数列表不同的函数。其实这里用模板更好,后面会介绍。

#include <iostream>
#include <stdlib.h>
using namespace std;
// 以下两个函数构成重载
int Add(int x, int y)
{
	return x + y;
}

double Add(double x, double y)
{
	return x + y;
}

int main()
{
	cout << Add(1, 2) << endl;     // 调用int的加法函数,输出3
	cout << Add(1.1, 2.2) << endl; //调用double的加法函数,输出3.3
	return 0;
}

构成函数重载有三种方式:参数的个数不同,参数的类型不同,参数的类型顺序不同

4.1、函数的参数个数不同构成重载

在这里插入图片描述

4.2、函数的参数类型不同构成重载

在这里插入图片描述

4.3、函数的参数类型顺序不同构成重载

在这里插入图片描述

这里还要注意:返回值不同是不构成函数重载的——调用的时候无法区分。以下还有一个例子:他们构成重载但是调用会出现歧义

#include <iostream>
#include <stdlib.h>
using namespace std;
void Func()
{
	cout << "void Func()" << endl;
}

void Func(int a = 0)
{
	cout << "void Func(int a = 0)" << endl;
}
int main()
{
	Func(1); // 传参调用不会出现歧义
	Func(); // 由于有缺省值,所以调用出现歧义,但是他们是构成函数重载的
	return 0;
}

4.4、函数重载的原理

接下来看看在Linux下g++编译器中函数重载的原理:
C和C++程序的代码如下:
在这里插入图片描述

在这里插入图片描述
我们进行编译后通过objdump -S查看汇编代码:
在这里插入图片描述
可以看到C语言编译出来函数的在func.o符号表中的函数名就是Func

在这里插入图片描述
而C++不是直接用函数名来标识的,这里的_Z是统一的前缀,4代表的是Func的长度,i代表int,c代表的是char,所以C++中构成函数重载的函数在func.o符号表中的名字是不一样。

C语言不支持函数重载,因为编译的时候,两个重载函数函数名是一样的,在func.o符号表中存在歧义和冲突,链接的时候也存在歧义和冲突,因为C语言是直接用函数名去标识和查找的。而重载函数的函数名一样,无法区分。
C++支持函数重载,因为C++符号表中并不是直接用函数名来标识和查找函数,C++有了函数名修饰规则(不同编译器下规则不同,以上举的是g++的例子),有了函数名修饰规则,func.o符号表中重载的函数就不存在歧义和冲突,其次链接的时候查找的也是明确的。

5、引用

C++中有了引用的概念,在部分场景下可以替代指针,在语法层面上我们认为:引用是给变量起别名,并没有开辟新空间。先来看引用的语法:

	类型& 引用变量名(对象名) = 引用实体;
	int a = 10;
	int& b = a;  // 定义引用类型b,这里让b引用a,b就是a的别名

需要注意的是引用类型和引用的实体必须是同种类型的。

5.1、引用的特性

引用在定义的时候必须初始化:

	int a = 10;
	int& b;  // -> error
	int& c = a; // -> true

一个变量可以有多个引用:

	int a = 10;
	int& b = a;
	int& c = a;
	int& d = b; // 这里被a的引用b起别名,本质还是a的引用,b和d都是a的别名

引用一旦引用了一个实体,就不能再引用其他实体:

	int a = 10;
	int b = 20;
	int& c = a;
	c = b;
	cout << c << endl; // 输出20

这里的c=b是把b赋值给c,还是让c称为b的别名呢?这里其实是赋值

5.2、引用传参

在C语言,我们要交换两个变量的值得通过指针来修改,有了引用之后就不需要指针了,而且传参还不用取地址,并且它们构成函数重载

#include <iostream>
#include <stdlib.h>
using namespace std;

void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int a = 0, b = 1;
	int c = 1, d = 0;
	swap(&a, &b); // 通过指针交换变量的值
	cout << a << " " << b << endl;
	swap(c, d);   // 通过引用交换变量的值,不需要&
	cout << c << " " << d << endl;
	return 0;
}

在这里插入图片描述

引用做参数还可以提高效率,当传参是一个大对象或者深拷贝时,可以减少拷贝提高效率,接下来我们测试用引用传参和普通传参的效率差别:

#include <iostream>
#include <stdlib.h>
using namespace std;

#include <time.h>
struct A { int a[100000]; };

void TestFunc1(A a) {}

void TestFunc2(A& a) {}

void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();

	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();

	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

int main()
{
	TestRefAndValue();
	return 0;
}

在这里插入图片描述
这里我们可以看到引用传参的效率更高。

引用传参的另一个意义是作为输出型参数将结果带出:

// C语言通过指针将余数r带出
int div(int x, int y, int* r)
{
	if (y == 0) return -1;
	*r = x % y;
	return x / y;
}
// C++通过引用将余数r带出
int div(int x, int y, int& r)
{
	if (y == 0) return -1;
	r = x % y;
	return x / y;
}

5.3、引用作返回值

与作参数相同,引用返回可以提高效率,因为不需要拷贝。

#include <iostream>
#include <stdlib.h>
using namespace std;

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
	return 0;
}

在这里插入图片描述
测试结果还是引用快很多,所以引用传参和作返回值都可以提高效率。

另外,引用作返回值,不仅可以获取返回值,还可以修改返回值

#include <iostream>
#include <stdlib.h>
using namespace std;

struct stack
{
	int a[100];
	int top;
	int capacity;

	int& at(int pos)
	{
		return a[pos];
	}
};
int main()
{
	stack st;
	st.at(0) = 10;
	return 0;
}

这里我们直接调用at函数获取0下标的元素,然后将其修改成10。由于是传引用返回,所以可以直接修改a[0]的值。在c++中结构体升级成了类,可以在类中定义成员函数和成员变量,这个我们下一章会讲解。现在我们知道引用返回可以获取返回值,还可以对返回值做修改就可以。

5.4、常引用

	int a = 10;
	int& b = a;   
	const int& c = a;

上面的a是可读可写的,b也是可读可写的,而c是只读的,b就是权限的平移,而c是权限的缩小


	const int a = 10;
	int& b = a; // error
	const int& c = a;

上面的a是只读的,而b是可读可写的,明显不合理,所以不能这么定义,这样就相当于权限的放大。
而c是只读的,相当于权限的平移,是允许的。

5.5、引用做返回值深入剖析

#include <iostream>
#include <stdlib.h>
using namespace std;

int Test()
{
	int n = 0;
	n++; 
	return n;
}

int main()
{
	int ret = Test(); // 1
	return 0;
}

上面是很简单的传值返回,返回n时会先把n的值拷贝给临时变量,然后再将临时变量的值拷贝给ret,一共是有两次拷贝。并且这里我们要知道临时变量具有常性——不可修改,临时变量是右值(C++11会介绍)。返回后Test函数的栈帧被销毁,里面变量开辟的空间也就不复存在了。如果返回的值比较小,是4/8个字节,会先存到寄存器中,再将寄存器的值拷贝给ret。


#include <iostream>
#include <stdlib.h>
using namespace std;

int Test()
{
	static int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Test(); // 1
	return 0;
}

这时候n存储在静态区,返回n还是会先生成一个临时变量,然后再把临时变量的值拷贝给ret。只不过返回后,由于n时存储在静态区,而不是存储在栈,所以n所开辟的空间并没有被释放。


#include <iostream>
#include <stdlib.h>
using namespace std;

int& Test()
{
	static int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Test(); // 1
	return 0;
}

这时候返回时会先生成n的引用作为临时变量,然后再将n的引用的值拷贝给ret,所以进行了一次拷贝。


#include <iostream>
#include <stdlib.h>
using namespace std;

int& Test()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Test();
	return 0;
}

上面的代码就是有问题的了。这时候返回先生成n的引用,然后将值拷贝给ret,但是函数调用结束会销毁栈帧,会将n开辟的空间还给内存,这时候再去访问n的值就是有问题的了。
如果栈帧销毁后没有清理栈帧,那么ret的值就是1,侥幸是正确的。
如果栈帧销毁后清理了栈帧,那么ret的值就是随机值。
所以这里ret的值是不确定的,在引用作为返回值时要注意,如果是在栈上开辟的变量,返回后就会被销毁,空间被释放,这时候返回引用就会出问题。


int& Test()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int& ret = Test();
	return 0;
}

这时候ret的值就是上面所说的不确定的。但是这里我们侧重点不在这,这里返回n先生成n的引用,然后再让ret成为n的引用的引用,所以最后ret本质还是n的引用,这里并没有发生拷贝。


#include <iostream>
#include <stdlib.h>
using namespace std;

int& Test()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int& ret = Test();
	cout << ret << endl;
	printf("ssssssssssssssssss\n");
	cout << ret << endl;
	return 0;
}

在这里插入图片描述

解释:上面调用Test后栈帧销毁,但是vs并没有清理栈帧,所以ret的值侥幸是1。这时候再调用printf函数,printf函数的栈帧会覆盖之前Test函数的栈帧,所以ret的值就未知了。

总结:1、基本任何场景都可以使用引用传参。2、引用作返回值时要注意,出了函数作用域后对象不存在了,就不能引用返回,还在就可以用引用返回。

5.6、引用的原理

这是在vs下的一段代码:

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

	int* p = &a;
	*p = 30;
	return 0;
}

接下来运行后查看反汇编:
在这里插入图片描述

解释:首先看引用的汇编,lea相当于是取地址,先将a的地址放到eax寄存器中,然后将eax的值给b。
将b的值放到寄存器eax中,然后[eax]就相当于解引用然后赋值20,这里的14h代表十六进制,转换成十进制就是20。
再看指针的汇编代码,发现和引用是一样的,所以我们可以得出:引用的底层就是指针。

5.7、指针和引用的区别

1、指针的指向是可以改变的。引用一旦引用了一个实体,就不能再引用其他实体。
2、指针在定义的时候可以不初始化,引用定义时必须初始化
3、有空指针,但没有空引用。有多级指针,但没有多级引用
4、sizeof中含义不同,指针计算的始终是4/8个字节(取决于32/64位平台),引用计算的结果是引用类型的大小。
5、指针访问实体需要解引用,而引用不需要。
6、引用比指针更加安全

6、内联函数

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

当我们需要频繁调用一个函数时,比如Add函数:

int Add(int x, int y)
{
	return x + y;
}

由于Add函数需要被频繁调用,比如要调用十万次,那么就需要不断的开辟栈帧,这样消耗就比较大。在C语言我们可以通过来解决:

#include <iostream>
using namespace std;

#define Add(x, y) ((x)+(y))
int main()
{
	int x = 10, y = 20;
	cout << Add(x, y) << endl;
	return 0;
}

但是宏很容易出错,比较复杂,比如上面的Add宏,x与y的一个括号都不能少,否则就有可能出问题。
在C++中我们可以在函数前面加上inline表示这是内联函数

inline int Add(int x, int y)
{
	return x + y;
}

在vs下面我们可以调试查看反汇编:
在这里插入图片描述
而不加inline是这样的:
会call一个地址


当然VS默认查看到的还是会call一个函数,这时候需要修改两个属性,首先右击项目选择属性,然后修改C/C++下常规中的调式信息格式为程序数据库,再把优化中的内联函数扩展改为只适用于inline。修改完之后添加inline就不会看到call了。

在这里插入图片描述在这里插入图片描述

另外inline不支持声明和定义分离,如果是.h文件和.cpp文件一起的,那就直接在.h文件中定义就好。
inline对于编译器而言只是一个建议,不同编译器对于inline的实现机制可能不同。一般我们建议对于函数规模小的,频繁调用的并且不是递归的函数添加inline。

7、auto关键字

auto关键字可以自动推导类型:

#include <iostream>
#include <stdlib.h>
using namespace std;

int main()
{
	auto a = 10;
	auto b = 1.11;
	auto c = 'c';
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	return 0;
}

在这里插入图片描述

这里我们用typeid打印出类型,只需要知道用法即可。但是这里作用还不是很大,接下来举一个更好的例子,下面的例子看不懂没关系,后面会做介绍。下面的例子用auto关键字就很方便:

#include <iostream>
#include <stdlib.h>
#include <string>
#include <unordered_map>
using namespace std;

int main()
{
	std::unordered_map<std::string, std::string> m;
	std::unordered_map<std::string, std::string>::iterator it = m.begin();
	auto mit = m.begin(); // 这里用auto关键字自动推导类型就可以很简便的写出来
	return 0;
}

auto对于指针和引用:

	int a = 10;
	auto p = &a;
	auto* pp = &a; // 指针可加*也可不加
	auto& b = a; // 引用必须加&

auto在同一行声明的变量必须是相同的类型,否则会报错:

	auto a = 1, b = 2;
	auto c = 1, d = 1.1; // error c和d的类型不同

auto不能声明数组,也不能作为函数参数:

	auto a[] = { 1, 2, 3 }; // error
	void Test(auto a)  // error
	{

	}

8、范围for

在早期C / C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
实际上范围for的底层是迭代器,这个我们在后面讲STL容器的时候会做介绍

在以前,我们要遍历数组和修改数据是这样写的:

#include <iostream>
#include <stdlib.h>
#include <string>
#include <unordered_map>
using namespace std;

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7 };
	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;

	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		a[i]++;
	}

	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		cout << a[i] << " ";
	}
	return 0;
}

在这里插入图片描述


C++11有了范围for和auto关键字我们还能这么写:

#include <iostream>
using namespace std;

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7 };
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;

	for (auto& e : a)
	{
		e++;
	}

	for (const auto& e : a)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

上面可以用auto来自动推导类型,也可以用int指明类型。但是需要注意的是:修改数据时得用引用,如果不用引用就是拷贝了,实际上并不会修改数组里的值。


#include <iostream>
using namespace std;

void Test(int* a)
{
	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	int a[] = { 1, 2, 3, 4, 5, 6, 7 };
	Test(a);
	return 0;
}

上面的代码是错误的,因为这时候a已经是指针了,不知道a的范围,范围for无法使用

9、指针空值nullptr

先来看这段代码:

#include <iostream>
using namespace std;

void test(int)
{
	cout << "void test(int)" << endl;
}

void test(int*)
{
	cout << "void test(int*)" << endl;
}

int main()
{
	test(0);
	test(NULL);
	test(nullptr);
	return 0;
}

在这里插入图片描述

可以看到0和NULL都是调用参数为int类型的函数,而nullptr是int*类型的函数
NULL本质上是一个宏,可能被定义为字面常量0或者是无类型指针(void*)0。
所以C++11引入了用nullptr表示指针空值,无需包含头文件。

至此有关C++入门的介绍结束,下一篇是类和对象。
如有问题欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值