【C++】C++入门2.0

各位读者老爷好,本鼠最近浅学了一点C++的入门知识!利用本博客作为笔记的同时也希望得到各位大佬的垂阅!

目录

1. 引用

1.1.引用的概念

1.2.引用的特性

1.3. 常引用

1.4.引用的使用场景 

 1.5.引用的易错点

1.6.引用的优势

1.7.引用和指针

2.内联函数

2.1.内联函数的概念

2.2.内联函数的特性

3. auto关键字(C++11)

3.1.auto的概念

 3.2.auto的使用细则

4.基于范围的for循环(C++11)

4.1.范围for循环的语法

4.2.范围for的使用条件

5.指针空值nullptr(C++11)


1. 引用

1.1.引用的概念

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

“取别名”的方法:类型& 引用变量名(对象名) = 引用实体。如下例所示:

#include<stdio.h>
int main()
{
	int a = 10;
	int& ra = a;//引用,ra就是a的别名
	printf("%p\n", &a);
	printf("%p\n", &ra);
	return 0;
}

 我们看到ra和a的内存空间确实是同一块。

注意:引用类型必须和引用实体是同种类型的。如上,变量a和ra的类型都是int。

1.2.引用的特性

1.引用在定义时必须初始化,就是必须说清楚是谁的别名。

2.一个变量可以有多个引用,就是说一个变量可以取多个别名。

3.引用一旦引用一个实体,再不能引用其他实体,就是说引用不能改变指向。

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int b = 20;
	//int& ra; 这条语句在编译时会出错,因为引用在定义时没有初始化
	int& ra = a;
	int& rra = a;
	rra = b;//这里是将b赋值给a的别名rra,可不是改变rra的指向
	cout << rra << endl;
	cout << &a << endl;
	cout << &ra << endl;
	cout << &rra ;
	return 0;
}

 

1.3. 常引用


我们知道了引用的概念,但是还必须知道常引用。我们看一个代码:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;
	const int c = 10;
	int& d = c;//error C2440: “初始化”: 无法从“const int”转换为“int &”
	return 0;
}

主函数第四条语句编译时会报错,为啥子捏?

原因:俺们看第三条语句,c是被const修饰了,c是常变量,具有常性,不可被更改。那么第四条语句如果通过的话明显是权限放大的表现:可以通过c的别名d更改c啊!显然这种权限放大的行为不行的。

那么常量想要取别名怎么办?

其实用const修饰别名就好了,这就是常引用,如下:

#include<iostream>
using namespace std;
int main()
{
	const int c = 10;
	const int& d = c;
	return 0;
}

所以我们要知道取别名不能放大权限(权限只能平移和缩小,不可放大)!


 知道了常引用,如果我们有一种场景:本身是可以改变的,但不希望通过别名改变本身。那么我们就可以使用常引用。这是一种权限的缩小。如:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;
	const int& c = a;
	b++;
	c++;// error C3892: “c”: 不能给常量赋值
	return 0;
}

这里b和c都是a的别名,不过c被const修饰了。通过b改变a是没问题的,但像通过c改变a编译时就会报错!


我们再看一些代码会对常引用有更深的理解:

代码1:

#include<iostream>
using namespace std;
int main()
{
	const int& a = 10;
	int& b = 10;//error C2440: “初始化”: 无法从“int”转换为“int &”
	return 0;
}

这里a就是常量10的别名,这里常引用使用的没毛病!但是第二条语句编译报错,原因还是权限放大了:如果编译通过就可以通过10的别名b更改10。

代码2:

#include<iostream>
using namespace std;
int main()
{
	double a = 13.14;
	int& b = a;//error C2440: “初始化”: 无法从“double”转换为“int &”
	return 0;
}

这里为什么会报错呢?

但是将b用const修饰之后编译就通过了(当然有警告),这又是为啥?

#include<iostream>
using namespace std;
int main()
{
	double a = 13.14;
	const int& b = a;
	return 0;
}

要搞清楚为啥,我们还要看一个代码:

#include<iostream>
using namespace std;
int main()
{
	double a = 13.14;
	int b = a;
	cout << b;
	return 0;
}

 

这个代码编译也通过了(当然也有警告)。那么是如何将double类型的数据赋值给int类型的数据的捏?

表面上好像就是a直接赋值给b呗。但是其实是产生了一个临时变量,先通过整形提升将a转换为整形,再将a赋值给临时变量,临时变量就是整形类型了,临时变量再赋值给b的。

我们还要知道临时变量也是具有常性的。

铺垫到这里,我们就可以解释这个代码为啥编译可以通过了,代码如下:

#include<iostream>
using namespace std;
int main()
{
	double a = 13.14;
	const int& b = a;
	return 0;
}

a和b之间也隔了一个临时变量,临时变量(类型是const int)具有常性。其实是具有常性的临时变量作为了被const修饰的b的初始化,也就是说,b是临时变量的引用,不是a的引用。当然没问题了。 但是如果不是常引用的话,权限就被放大了,就会报错!!

代码3:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	const int& b = a + 100;
	int& c = a + 100;//error C2440: “初始化”: 无法从“int”转换为“int &”
	return 0;
}

 为什么第二条语句编译通过,第三条语句编译报错?

其实是因为a+100是表达式,该表达式的返回值是一个临时变量,临时变量具有常性。所以给临时变量取别名必须是常引用。

1.4.引用的使用场景 


1.做参数

举个例子:

#include<iostream>
using namespace std;
void swap(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	swap(a, b);
	cout << "a=" << a << endl;
	cout << "b=" << b;
	return 0;
}

用引用的方式接受实参,这里left就是a的别名,right就是b的别名,通过别名直接进行交换。就不用通过地址解引用间接来进行交换了。 

  

当然,如果实参不希望被引用改变时,可以使用常引用,就是用常引用的方式来接受实参。其实不是输出性参数最好都用常引用。 


2.做返回值

 举个例子:

#include<iostream>
using namespace std;
int& Func()
{
	static int n = 10;
	n++;
	return n;
}
int main()
{
	cout << Func() ;
	return 0;
}

这里Func函数的返回值是通过引用返回的哦,返回的是n的别名。

 


 1.5.引用的易错点


1.明确概念,举个例子:

#include<iostream>
using namespace std;
int& Func()
{
	static int n = 10;
	return n;
}
int main()
{
	int& ret = Func();
	int tmp = Func();
	return 0;
}

我们都知道函数Func返回的是n的别名。那么ret和tmp分别是什么?

其实ret是n别名的别名;tmp是将n的别名的值赋给tmp。我们可以验证:

通过调试我们可以看到ret和n是同一块内存空间,tmp的空内存间却与它们不同:

再看:

#include<iostream>
using namespace std;
int& Func()
{
	static int n = 10;
	return n;
}
int main()
{
	int& ret = Func();
	int tmp = Func();
	ret++;
	cout << ret << endl;
	cout << tmp;
	return 0;
}

 

 我们看到tmp的值确实是10。而且ret++不影响tmp,因为tmp不是n的别名,也不是ret的别名,更不是n的别名的别名,所以ret++不会影响tmp。


2.如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。为什么这么说,我们看一个例子:

#include<iostream>
using namespace std;
int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}

先看结果也许会出乎你的意料:

 

结果是7的原因:我们看到Add函数返回值是c的别名。但是局部变量c出了Add函数作用域之后局部变量c的内存空间使用权已经还给操作系统了,那么局部变量c的内存空间存储的数据是什么就不好说了,这是返回c的别名就是一个野引用。我们看主函数:ret是c别名的别名,但是c的别名是一个野引用。但再次调用Add函数时(Add(3,4);),系统复用了局部变量c的内存空间,导致局部变量c的内存空间存储的值时7.所以打印出来的ret是7。

解决上面问题的方法就是传值返回,而不是使用引用返回,如上代码写法是错误的!


1.6.引用的优势

作为参数或者返回类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用作为参数或者返回类型,效率是非常低下的,尤其是当参数或者返回类型非常大时,效率就更低。


1.引用做参数效率的提高

#include <time.h>
#include<iostream>
using namespace std;
struct A 
{ 
	int a[10000];
};
void TestFunc1(A a)
{

}
void TestFunc2(A& a)
{

}
int main()
{
	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 ;
	return 0;
}

在Debug版本下结果:

 


 2.引用做返回值效率的提高

#include <time.h>
#include<iostream>
using namespace std;
struct A
{ 
	int a[10000]; 
};
A a;
// 值返回
A TestFunc1() 
{ 
	return a;
}
// 引用返回
A& TestFunc2() 
{ 
	return a; 
}
int main()
{
	// 以值作为函数的返回值类型
	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 ;
	return 0;
}

在Debug版本下结果:

 


1.7.引用和指针

引用和指针的功能是类似的、重叠的。C++的引用是对指针使用比较复杂的场景进行一些替换,让代码更加易懂,但是引用不能完全替代指针,因为:引用一旦引用一个实体,再不能引用其他实体,就是说引用不能改变指向。

引用和指针的区别:

1.在语法层面:

  • 引用是别名,不开空间;指针是地址,需要开空间存地址。(要注意语法层面来说引用不开空间,但是在底层其实开了空间,因为引用的语法含义和底层实现是背离的,引用底层是用指针实现的)
  • 引用定义后必须初始化,指针可以初始化也可以不初始化。
  • 引用一旦引用一个实体,再不能引用其他实体,就是说引用不能改变指向,但指针可以。
  • 引用相对更安全。没有空引用,但有空指针;容易出现野指针,不容易出现野引用。

2.在底层:在汇编层面上,没有引用,都是指针,引用编译后也转换成指针了。


学完引用,看看能不能看明白这个代码:

typedef struct Node
{
	struct Node* next;
	struct Node* prev;
	int val;
}Node, * PNode;
void PushBack(PNode& phead, int x)
{
	//…………
}
int main()
{
	PNode plist = nullptr;
	PushBack(plist, 10);
	return 0;
}

首先:struct Node被typedef成Node,struct Node*被typedef成PNode。 

主函数:定义了一个类型是PNode的指针变量plist,并初始化成空指针。将plist和10作为实参传入PushBack。

PushBack函数:phead是plist的别名,phead类型是PNode(也是struct Node*)。


2.内联函数

引子:如果有一个函数要频繁调用100w次,那么需要建立100w次栈帧。如何解决这个问题呢?


C语言给出的答案就是宏。说起宏,如何用宏实现下面代码?

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

答案如下:

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

 用宏实现函数功能要注意下面几点:

  • 宏是一种替换:宏在预处理阶段进行了替换。
  • 宏不是函数,没有返回值和参数。
  • 必须用括号控制好运算的优先级
  • 宏定义在行末不加分号。

宏的优缺点:

 优点: 1.增强代码的复用性。 2.提高性能。

缺点: 1.不方便调试宏。(因为预编译阶段进行了替换) 2.导致代码可读性差,可维护性差,容易误用。 3.没有类型安全的检查 。4.语法复杂,不好控制。


而C++给出的答案是内联函数,可以避免宏的缺陷。

2.1.内联函数的概念

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

像这样:

inline int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int sum = Add(1, 2);
	return 0;
}

如上述Add函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的 调用。

既然C++有内联函数的概念,我们是不是将所有函数都加上inline,这样不就没有栈帧的消耗了吗?肯定不行,因为:

2.2.内联函数的特性

1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会 用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。

2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。就是说内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。

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

3. auto关键字(C++11)

3.1.auto的概念

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

说简单点,auto作用就是自动推导变量类型的,如:

#include<iostream>
using namespace std;
int main()
{
	double a = 13.14;
	auto b = a;
	auto c = 1;
	auto d = 'D';
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() ;
	return 0;
}

简单介绍一下,typeid().name()可以获取变量类型,用法如代码。我们看看结果:

 

 3.2.auto的使用细则


1.使用auto定义变量时必须对其进行初始化

#include<iostream>
using namespace std;
int main()
{
	auto a;//error C3531: “a”: 类型包含“auto”的符号必须具有初始值设定项
	return 0;
}

不初始化无法通过编译。


2.auto与指针和引用结合起来使用 

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

#include<iostream>
using namespace std;
int main()
{
	double a = 13.14;
	auto& b = a;
	auto c = &a;
	auto* d = &a;
	cout << &a << endl;
	cout << &b << endl;
	cout << c << endl;
	cout << d;
	return 0;
}

变量a和b的内存空间是同一块,也就是指针变量c或者d所指向的那块。

 


3.在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译 器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

void TestAuto()
{
    auto a = 1, b = 2; 
    auto c = 3, d = 4.0;  // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

4.auto不能作为函数的参数 

auto不能作为形参类型,因为编译器无法对a的实际类型进行推导

#include<iostream>
using namespace std;
// error C3533: 参数不能为包含“auto”的类型
int Add(auto a, auto b)
{
	return a + b;
}
int main()
{
	int i=Add(1, 2);
	return 0;
}

5.auto不能直接用来声明数组

int main()
{
	int a[] = { 1,2,3 };
	auto b[] = { 4,5,6 };//错误写法
	return 0;
}

4.基于范围的for循环(C++11)

4.1.范围for循环的语法

C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。 

意思是依次取数组元素的值赋值给范围内用于迭代的变量,自动迭代,自动判断结束。

举个栗子:

#include<iostream>
using namespace std;
int main()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (auto& e : array)
		e *= 2;

	for (auto e : array)
		cout << e << " ";
	return 0;
}

 注意:第一个for循环我们期待能改变(*2)数组元素,所以得使用引用。

 

当然与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环 。

4.2.范围for的使用条件

1. for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin和end的方法,begin和end就是for循环迭代的范围。

以下代码的for循环迭代的范围就是不确定的,是错误的写法:

void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}

因为array实质是一个指针,而不是数组。

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

5.指针空值nullptr(C++11)

C语言当中用NULL表示空指针,而C++用nullper表示空指针!

注意:

1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

2.. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

6.小知识

6.1.小知识1

 我们来看一个工程,该工程下有3个文件如下:

  • Add.h

Add函数的声明和定义都写在了Add.h文件当中了。

#pragma once
int Add(int a, int b)
{
	return a + b;
}
  • Add.cpp
#include"Add.h"
  • test.cpp
#include"Add.h"
int main()
{
	int a = 10, b = 10;
	int c = Add(a, b);
	return 0;
}

我们对这个工程生成解决方案,就报错了:test.obj : error LNK2005: "int __cdecl Add(int,int)" (?Add@@YAHHH@Z) 已经在 Add.obj 中定义。

为什么会报错?

 因为在Add.h中声明和定义了Add函数。由于Add.cpp和test.cpp中都包含了Add.h,所以Add.cpp和test.cpp都有了Add函数的声明和定义。对这个工程生成解决方案时,Add.cpp会生成Add.obj,test.cpp会生成test.obj, 当链接的时候会将这两个.obj文件链接到一起,这两个.obj文件各有一个Add函数的声明和定义且不构成函数重载,所以编译器认为定义了重复的函数,就报错了。

如何解决呢,有三个办法:

  • 办法1:Add函数的声明和定义分离。Add函数的声明放在Add.h文件中,Add函数的定义放在Add.cpp文件中。这也是声明和定义分离的意义:防止链接冲突。当声明和定义分离时,生成的.obj文件中只有一个.obj文件有Add函数的定义,当两个.obj文件链接到一起时,Add函数的声明和定义都有了,而且只有一份。
  • 办法2:Add函数的声明和定义可以不分离,都放到Add.h文件中,但是要用static修饰Add函数使之成为静态函数。static修饰函数的时候会改变函数的连接属性,使函数只在当前文件可见,可以避免链接冲突。
  • 办法3:Add函数的声明和定义可以不分离,都放到Add.h文件中,但是要用到inline修饰Add函数使之成为内联函数。原理跟static修饰Add函数差不多,也可以避免链接冲突。

所以当我们就想要在.h文件中包含函数的声明和定义时,规模大的函数我们就用static修饰,规模小的函数我们就用inline修饰,就可以避免链接冲突,例子我就不举了,有兴趣可以去试试。

这里也说明了内联函数为什么声明和定义不能分离了。如果内联函数的声明和定义分离,内联函数定义只在当前.cpp文件可见,当调用内联函数的.cpp文件生成的.obj文件想要通过链接取找到内联函数的定义是找不到的。

6.2.小知识2

关键字static是用来修饰变量和函数的:

修饰局部变量——称为静态局部变量:static修饰局部变量改变了变量的生命周期 让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。

修饰全局变量——称为静态全局变量:一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。

修饰函数——称为静态函数:一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。

感谢阅读!

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

X_chengonly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值