【C++】入门必备小知识


1. 域

  1. 域就是作用域,同一个域不可以用同名的变量,不同域可以用同名变量,遵循局部优先。域包括局部域、全局域、类域、命名空间域。其中我们学过局部域和全局域,类域和命名空间域后面会讲到。
  2. 域的访问
int a = 0;
int main()
{
	int a = 1;
	printf("%d", a);//a的作用域是局部域
	//如何打印全局域的a?
	//需要用到域作用限定符::
	printf("%d", ::a);
	return 0;
}

2. 命名空间

在C语言中,我们定义的变量、函数的名字在全局域可能与库中函数发生冲突,或者跟其他文件项目的变量名发生冲突。而C++的命名空间就可以解决这个问题。命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染

2.1命名空间的定义

namespace 命名空间的名字
{
	命名空间的成员
}
  1. 命名空间的成员可以是变量、函数、自定义类型。
//例子
namespace nanmu
{
	int a;
	void Swap(int*p1,int*p2)
	{
		int tmp = *p1;
		*p1 = *p2;
		*p2 = tmp;
	}
	struct student
	{
		char name[20];
		int age;
	}
}
  1. 命名空间可以嵌套定义
namespace N1
{
	int a;
	int b;
	namespace N2
	{
		int a;
		int b;
		int Add(int x,int y)
		{
			return x+y;
		}
	}
}
  1. 同一个工程允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间。例如一个.h文件和一个.c文件都定义了一个命名空间N1,最终都会合并成一个。
    注意
    名称相同的命名空间合并后有两个同名的变量/函数/自定义类型,就会报错(同一个作用域不可以用同名的函数)。怎么解决?再定义一个命名空间嵌套。

  2. 命名空间域是命名空间成员的作用域。

2.2 命名空间的使用

我们要如何使用命名空间的成员?以下是三种命名空间的使用方式:

  1. 使用using namespace 命名空间的名称(全部展开)
using namespace std;
//std是C++标准库的命名空间名,它包括C++库和STL(以后会学到)
int main()
{
	printf("hello,c++");
}

直接展开命名空间后,如果我们定义和库重名,就会发生冲突。所以就有第二和第三种方式。

  1. 使用using将命名空间的某个成员引入(展开某个)
using N1 a;
int main()
{
	printf("%d",b);
}
  1. 使用命名空间+作用域限定符+成员访问
int main()
{
	printf("%d",N::a);
}

结论:直接展开有风险,我们定义如果跟库重名,就会报错。建议项目里面不要展开,日常练习可以直接展开。


3. C++输入和输出

using namespace std;
#include<iostream>
int main()
{
	int a = 0;
	cin >> a;//从键盘输入a
	cout << a << endl;//打印a到屏幕
	return 0;
}

cin是标准输入对象(键盘),cout是标准输出对象(控制台),endl是C++符号,表示换行输出,<<是流插入符号,>>是流提取符号。< iostream >是cin、cout、endl的头文件。

  1. 跟C语言的输入输出对比(scanf,printf),C++的输入输出更方便,不用手动控制格式,C++会自动识别变量类型
int main()
{
	char a;
	int b;
	double c;
	cin >> a >> b >> c;
	cout << "char:" << a << endl << "int:" << b << endl << "double:" << c << endl;
	return 0;
}

在这里插入图片描述

  1. scanf要快于C++的输入(cin)。原因是C++兼容C,要在缓冲区兼顾C和C++的输入。

4. 缺省参数

如果我们在函数传参时,忘记传实参或者少传参了怎么办?C语言会报错,而C++提出了缺省参数。

  1. 概念
    缺省参数(又叫默认参数)是在函数声明和定义时为函数参数指定一个缺省值(默认值)。
  2. 作用
    在函数传参时,如果没有指定实参就使用该参数的缺省值。如果给定实参就不使用
int Add(int x = 1, int y = 1)
{
	return x + y;
}
int main()
{
	int ret = Add();
	cout << ret << endl;
	return 0;
}

结果
在这里插入图片描述

  1. 分类
//全缺省参数(所有参数都给缺省值)
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
//半缺省参数(缺省部分参数)(至少需要传一个参数)
void Func2(int x , int y = 2)
{
	cout << "x = " << x << endl;
	cout << "y = " << y << endl;
}
int main()
{
	Func1();
	Func2(2);
	return 0;
}

结果
在这里插入图片描述

  1. 注意
    (1)我们给参数缺省值的时候,必须从右往左给,因为我们传参时是从左往右传。
    (2)缺省参数不能在函数声明和定义中同时出现。如果我们声明和定义的函数参数的缺省值不一样,编译器就无法确定哪个才是真正的缺省值。
    (3)那么声明和定义都给相同的缺省值?还是只给一个缺省值就够了?答案是只给一个就够了。那么是声明给缺省值,还是定义给缺省值?答案是声明给缺省值。定义给缺省值就会报错,在编译阶段,文件会包含头文件,此时头文件中的函数声明未给缺省参数,就会报错,所以是声明给。
    (4)缺省值必须给常量或者全局变量
    在这里插入图片描述
  2. 应用
struct Stack
{
	int* a;
	int top;
	int cap;
};
void StackInit(struct Stack* p, int capa = 10)
{
	……
}
int main()
{
	struct Stack s1;
	StackInit(&s1,100);//如果我们已知道要建立一个大小为100的栈,这时就直接将参数传过去
	struct Stack s2;
	StackInit(&s2);//如果不知道要创建大小为多少的栈,这时可以不传参,用缺省值
	return 0;
}

5. 函数重载

  1. 概念
    C++允许同一作用域出现同名函数,这些函数的参数个数、参数类型、参数顺序不同,构成功能类型的重载函数。(对函数的返回值没有要求,可相同可不同)
//函数重载
int Add(int x, int y)
{
	return x + y;
}
double Add(double x, double y)
{
	return x + y;
}
int main()
{
	int ret1 = Add(1, 2);
	double ret2 = Add(1.5, 2.5);
	cout << "ret1 = " << ret1 << endl;
	cout << "ret2 = " << ret2 << endl;
	return 0;
}

结果
在这里插入图片描述

  1. 分类
//1.参数类型不同
int Add(int x, int y)
{
	return x + y;
}
double Add(double x, double y)
{
	return x + y;
}

//2.参数个数不同
void Func1(int a)
{
	cout << "Func1(int a)" << endl;
}
void Func1(int a, int b)
{
	cout << "Func1(int a,int b)" << endl;
}

//3.参数顺序不同
void Func2(int a, double b)
{
	cout << "Func2(int a,double b)" << endl;
}
void Func2(double a, int b)
{
	cout << "Func2(double a ,int b)" << endl;
}
  1. 注意
    (1)调用歧义
vodi f()
{
	cout<<"void f()"<<endl;
}
void f(int a = 0)
{
	cout<<"void f(int a = 0)"<<endl;
}
int main()
{
	f();
}

首先,f()和f(int a =0)是重载函数。但是,你能明白f()调用哪个函数吗?它既可以解释为不传参的函数,此时它调用的是有缺省值的函数;也可以理解为无参数的函数,此时它调用的是无参数的函数。两个都可以调用就存在歧义,编译器就会报错。
(2)函数名问题

void f(int a,int b)
{
	……
}
void f(int b,int a)
{
	……
}

这是否构成函数重载?不构成,这并不是函数参数的顺序不同,它跟形参的名字不同无关。

  1. 为什么C++支持函数重载?
    后面补上。

6. 引用

  1. 概念
    引用就是给一个变量取别名,既然是别名,那么和变量名一样都是指这个变量,所以引用变量和变量共用一块内存空间
int main()
{
	int a = 0;
	int& b = a;//b是a的别名
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

打印结果
在这里插入图片描述

  1. 特点
//1.引用在定义的时候必须初始化
int main()
{
	int a = 10;
	int& b;//错误的
	int& b = a;
}
//2. 一个变量可以有多个引用
int main()
{
	int a = 0;
	int &b = a;
	int &c = b;
	int &d = a;
	//如果b++,c++,d++,那么a也会++。因为他们都是共用同一块空间。
}
//3. 引用一旦引用其他变量,就不能再引用其他变量
int main()
{
	int a = 10;
	int&b = a;
	int c = 20;
	b = c;//这里是将d变成c的引用?还是将c赋值给d?
	//答案是将c的值赋值给b
}
  1. 应用
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = x;
}
int main()
{
	int x = 10;
	int y = 20;
	Swap(x, y);
	return 0;
}

形参是实参的别名(形参和实参共用一块内存空间),对形参的改变就是对实参的改变

  1. 适用场合
//1.做参数
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = x;
}
//好处:(1)形参的改变引起实参的改变;(2)提高效率,对于一些比较大的参数
//2.做返回值
int count()
{
	int n = 0;
	n++;
	return n;
}
//n返回时会生成临时变量,再返回。原因是变量出了作用域就销毁。

//如果返回的是静态变量呢?
int count()
{
	static int n = 0;
	n++;
	return n;
}
//此时变量出了作用域就不销毁了,但还是会临时变量。
//不管是局部变量还是临时变量,如果是传值返回,都会生成临时变量。

//那怎样才能不生成临时变量?不生成临时变量有什么用?
//(1)使用传引用返回,就不会生成临时变量。(2)减少拷贝,提高效率。如果返回是大对象,效率会提高得更明显。

//此时引用做返回值的好处就体现出来:减少拷贝,提高效率。
int& count()
{
	static int n = 0;
	n++;
	return n;
}

//疑问:如果count中的n是局部变量,不是静态变量,还可以用引用返回码?
//答案:不可以。局部变量出了作用域就销毁了,会非法访问,结果不可知。如果函数结束,函数栈帧销毁,没有清理栈帧,那么n的值侥幸是正确的。
int main()
{
	int& ret = count();
	return 0;
}

总结
(1)基本任何形参都可以用引用传参。
(2)谨慎引用做返回值。出了函数作用于,对象不在就不能引用返回,还在就可以用引用返回。正确的样例:静态变量,全局变量。

  1. 常引用(重点)
//常引用
int main()
{
	const int a = 10;
	int& b = a;//错误的。引用过程中,权限不能放大。a不能改变,却可以通过b来改变a,这是错误的。

	const int c = 10;
	int d = c;//正确的。这是一份拷贝,d的值的改变不影响c的值。

	int x = 0;
	int& y = x;//正确的。权限的平移。
	const int& z = x;//正确的。权限的缩小。引用过程中,权限可以平移或者缩小
	++x;//正确的。//缩小的是z作为别名的权限,x的权限不缩小。
	//此时z是否会发生变化?z也会发生变化,不能通过z修改x,但x改变,z是x的别名,也会随之改变。

	const int& m = 10;//正确的。因为常量具有常属性,不可以通过m改变10。

	double e = 1.11;
	int ii = e;//正确的。这是一份拷贝。但在拷贝过程中有类型转换,此时会产生临时变量。
	int& iii = e;//错误的。iii放大了权限,因为临时变量具有常性。e首先产生临时变量,再赋值给iii。
	const int& iii = e;//正确的。此时相当于权限的平移。

	return 0;
}

常引用还体现函数的返回值

int func1()
{
	static int x = 10;
	return x;
}
int& func2()
{
	static int x = 10;
	return x;
}
int main()
{
	int& ret1 = func1();//错误的。func1返回的是x的拷贝,是临时变量,具有常属性,此时相当于权限的放大
	const int& ret2 = func1();//正确的。权限的平移。
	int ret1 = func1();//正确的。返回一份拷贝。
	//注意:只有引用时才有权限的放大、缩小和平移。


	int& ret3 = func2();//正确的。权限的平移。
	const int& ret4 = fun2();//正确的。权限的缩小。
	return 0;
}

注意
只有类型转换的时候才会产生临时变量,相同类型不会产生临时变量。

  1. 引用和指针的区别
    (1)相同点
	int a = 10;
	int& aa = a;//在语法层面上,引用不开空间,是对a取别名
	int* pa = &a;//在语法层面上,指针开空间,存储a的地址

但在底层实现上,引用是类似指针的方式实现的。
在这里插入图片描述
(2)不同点

  1. 在概念上,引用定义一个变量的别名,指针存储一个变量的地址。
  2. 引用在定义式必须初始化,指针没有要求。
  3. 引用在初始化时引用一个实体后不能再引用其他实体,而指针可以在任何时候指向任何一个实体。
  4. 没有空引用,但有空指针。(但有野引用和野指针)。
  5. sizeof(引用)==引用类型的大小,sizeof(指针) ==4/8字节。
  6. 引用+1,实体就+1;指针+1,就往后偏移一个类型大小的距离。
  7. 有多级指针,但没有多级引用。
  8. 访问实体方式不同,指针需要显示解引用,引用编译器自己处理。
  9. 引用相对于指针而言更安全和便利。

7. auto

  1. 概念
    auto是一个类型指示符,用来自动识别类型。
  2. 例子
//auto关键字
int main()
{
	int a = 10;
	auto b = 20;
	auto c = 3.14;
	auto d = 'H';
	cout << typeid(b).name() << endl;//打印类型
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	return 0;
}

打印结果
在这里插入图片描述

  1. 注意
    (1)使用auto时必须初始化,auto能自动识别类型的本质是在编译阶段,编译器根据初始化表达式推导出变量的实际类型,然后将auto替换。
    (2)当auto在同一行定义多个变量时,要保持这些变量必须是相同的类型,否则编译器会报错。原因是编译器只推导第一个变量的类型,然后用推导出的类型定义其他变量。
int main()
{
	auto a = 1, b = 2, c = 3;//正确的
	auto x = 3.14, y = 4;//错误的
	return 0;
}

(3)当auto声明指针类型时,auto*和auto没有区别,但当auto声明引用类型时,就必须带上&。

	int a = 1;
	auto b = &a;
	auto* c = &a;
	auto& d = b;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

打印结果
在这里插入图片描述

(4)auto不能作为函数的参数
编译器无法对形参的实际类型进行推导
在这里插入图片描述
(5)auto不能直接用来声明变量
在这里插入图片描述

  1. 应用
    auto通常用来代替复杂的类型,还可以与范围for配合使用。

8. 范围for

  1. 概念
//范围for
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (auto e : arr)
		cout << e << " ";
	return 0;
}

打印结果
在这里插入图片描述
(1)从一个范围确定的数组中取出元素放在e中,然后打印,不管数组中元素的类型。这就是范围for。
(2)它会自动迭代,自动判断结束。
(3)for后面的括号分为两部分,冒号前是循环的变量,冒号后是循环的范围。
(4)这里的e是临时变量的名称,可以随便取。
(5)如果循环过程中要改变数组的内容,就得用引用类型,因为e是arr元素的临时拷贝,不改变arr。

for(auto&e:arr)
	e+=1;
  1. 注意
    for循环的范围必须是确定的。对于数组而言,范围就是第一个元素和最后一个元素。

9.nullptr空指针

NULL在C语言被定义为空指针,但在C++被定义为字面常量0。如下面的例子
在这里插入图片描述
所以就有了nullptr,它表示空指针。


10.内联函数

  1. 引入
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	for (int i = 0; i < 10000; i++)
	{
		cout << Add(i, i + 1) << endl;
	}
	return 0;
}

Add循环10000就需要建立10000个函数栈帧,如果要减少建立栈帧的次数,要如何解决?
用宏定义,宏的优点:不需要建立栈帧,提高调用效率。
但考虑到宏的缺点:(1)复杂、容易出错(2)可读性差,不能调试。所以C++推出内联函数。

  1. 定义
    用inline修饰的函数叫做内联函数。编译时C++编译器会在调用内联函数的地方展开,这样就没有函数调用产生的开销,提高运行效率。
inline int Add(int x, int y)//在函数钱前面+inline
{
	return x + y;
}
  1. 疑问
    是不是所有的函数都可以用内联函数?
    并不是。inline也有适用范围:短小的频繁调用的函数。函数太长会导致代码膨胀。
    例子

假设有一个函数Func(),编译后有50条指令。如果有一个项目在10000个位置调用该函数。
(1)第一种情况:Func不是内联函数,合计多少条指令?
项目中每个调用Func的地址都只有一条调用指令call,一共10000+50条指令。
(2)第二种情况:Func是内联函数,合计多少条指令?
Func每次调用都被展开,所以一共10000*50条指令。那么这个可执行程序就变大(例:安装包变大,或者升级程序变大)。
所以并不是所有的函数都可以变成内联函数。

  1. inline对于编译器仅仅是个建议,最终是否成为内联函数,由编译器决定。像比较长的函数、递归函数+inline后就会被编译器否决掉。

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

  3. 默认debug下面,inline不会起作用,否则函数都被展开了,无法进入函数,不方便调试。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值