初识C++

初识C++

一段C++版的hello world
初识C++_第1张图片
C++是在C的基础之上,容纳进去了面向对象编程思想,增加了许多有用的库,也弥补了许多C语言的不足。

命名空间

来解决C语言明明冲突的问题。

☀️C语言的命名冲突问题

当在C语言中定义一个名叫rand的变量,可能程序会出错,因为在stdlib.h库中,rand是一个函数,命名冲突了:
初识C++_第2张图片

☀️C++命名空间与域作用限定符

1.对命名空间内变量的访问

初识C++_第3张图片

定义一个名叫bit的命名空间,该空间内部有一个叫rand的变量,当我想使用该变量时,就用“空间名+域作用限定符+变量名“的方式”,即“bit :: rand”,当rand前面什么都没有时,默认从全局变量中找rand,即rand函数。

2.命名空间内可以放变量、函数、结构体等,都用 : : 符号来限定。

初识C++_第4张图片

3.命名空间的内部嵌套

如果同一个命名空间内部两名称冲突,那就在该空间内继续嵌套空间,隔开冲突的名称。
初识C++_第5张图片
假设大空间bit内部包含了两个小空间bit1和bit2,此时对小空间的访问方式是“大空间+小空间+域作用限定符+变量名”,对bit1空间内访问是“bit : : bit1: : rand”。
初识C++_第6张图片

4.合并命名空间

同一个文件的多个位置出现同一个命名空间,或多个文件中出现同一个命名空间,编译器会将他们合并。
例如:Stack.h文件的bit空间内声明了两个函数,Stack.cpp文件中的bit空间内是这两个函数的定义,Test.cpp文件中用域作用限定符对函数进行访问。由于编译器的合并命名空间功能,从而也可以实现分文件操作。
初识C++_第7张图片

初识C++_第8张图片
初识C++_第9张图片

5.默认指定展开

(1)整体展开

即默认指定全部命名空间。
如果要用bit空间内的变量、函数等,还要每次都要指定bit空间,即写“bit : : (…)”太麻烦了,而如果设置默认访问bit空间,即默认使用的每一个函数或者变量都是bit空间中的,就不需要麻烦的写“bit : : (…)”了。

指定命名空间语法:using namespace (空间名)
例:展开指定C++标准库定义的命名空间std:
初识C++_第10张图片
注:工程项目不要展开std,容易冲突,日常练习可以。最安全的方式是指定展开我们自己创建的命名空间,需要哪个展开哪个。

(2)部分展开

即默认指定命名空间内部分的变量、函数等。全部展开风险太大,部分展开可以规避风险。
cout是C++的输出流,cin是输入流,<<是流插入运算符,>>是流提取元运算符,都储存在空间std中。当我们不全部展开,只展开部分常用的,就可以在规避风险的同时实现便利。
例:部分展开前后对比:
当std不是默认指定空间时,用cout输出变量a和b:
初识C++_第11张图片
太麻烦,指定cout为默认访问:
初识C++_第12张图片
初识C++_第13张图片

C++输入&输出

cout是输出(流输出),cin是输入(流提取),cout、cin都是定义在头文件iostream中的。

☀️使用cout和cin打印与输入数据

将std库中的cout设为默认后,直接使用:
初识C++_第14张图片
初识C++_第15张图片

☀️C++可以兼容C

目前还不知道怎样用C++控制数据精度,可以借助C语言打印精度更高的数据:
初识C++_第16张图片

缺省参数

☀️缺省参数概念:

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。调用时可传参也可不传参,不传参时,参数值就是指定的缺省值。缺省值必须是常量或者全局变量。

☀️缺省参数的声明与定义

当函数既有声明又有定义时,缺省参数不可以在声明和定义中都出现,防止两个参数值不一样。也不能只在定义中出现声明中不出现,这样包含头文件时会不知道声明中的参数是几。只能单独在声明中出现。

.h文件中的声明里需要出现缺省值:
初识C++_第17张图片

.cpp文件中的定义里不可出现缺省值:
初识C++_第18张图片

☀️缺省参数的分类

1.全缺省参数

所有参数都是缺省参数,声明时需要指定默认值:

void Func(int a = 10, int b = 20, int c = 30)
 {
     cout<<"a = "<<a<<endl;
     cout<<"b = "<<b<<endl;
     cout<<"c = "<<c<<endl;
 }

2.半缺省参数

只有后半部分参数是缺省参数:

void Func(int a, int b = 10, int c = 20)
 {
     cout<<"a = "<<a<<endl;
     cout<<"b = "<<b<<endl;
     cout<<"c = "<<c<<endl;
 }

注:半缺省参数必须从右往左依次来给出,不能从左往右给,也不能间隔着给。(如果从前往后设置缺省参数或者间隔着设置,那么编译器会不知道哪个对应的是缺省参数)

☀️缺省参数应用

比如在栈初始化申请空间时,不论知不知道要多大的空间,都可以用同一个函数。具体方法是使用缺省参数设置缺省值,该值表示默认开辟的空间大小,知道需要多少空间就传参,不知道就不传参使用缺省值。
初识C++_第19张图片
初识C++_第20张图片

函数重载

☀️函数重载概念:

C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
注:返回值可同可不同,具体取决于返回的数据的类型。

类型不同:
初识C++_第21张图片
个数不同:
初识C++_第22张图片

☀️注:函数重载要避免歧义

Add有两个重载函数,第一个的两个参数都是int类型的,第二个的两个参数都是double类型的:
初识C++_第23张图片
此时,如果一个函数调用为Add(1,2),则会匹配到Add(int left,int right);如果一个函数调用为Add(1.1,2.2),则会匹配到Add(double left,double right)。但是当函数调用为Add(1,2.2)时,既可以匹配到Add(int left,int right)(此时会将第二个参数转换成int类型),也可以匹配到Add(double left,double right)(此时将第一个参数转换成double类型),有了歧义,这时编译器不知道匹配哪一个函数,会报错。
初识C++_第24张图片

初识C++_第25张图片

再比如,函数f有两个重载函数,第一个无参数,第二个有一个缺省参数:

void f()
{
 cout << "f()" << endl;
}
void f(int a=1)
{
 cout << "f(int a)" << endl;
}

此时,如果不传参调用f函数,可以匹配到第一个无参数的f函数,也可以匹配到第二个不传值情况下的缺省函数,产生歧义,编译器报错。不传参时调用存在二义性:
初识C++_第26张图片

引用()

☀️一、引用概念:

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。那么电脑中的同一块空间就有了三个名字:李逵、铁牛、黑旋风,通过这三个名字都可以找到这块空间。
同一个变量可以有多个别名,即多次引用:
初识C++_第27张图片
初识C++_第28张图片

☀️二、C的指针&C++的引用

C的传址调用:调用时需要传地址,函数内部接收到后需要解引用
C++的引用:调用时直接用变量名,函数内部以取别名的方式接收变量
初识C++_第29张图片
第一个Swap是C语言版本的传地址,第二个Swap是C++版本的引用。第二个Swap函数表示,left是a的别名,right是b的别名,left改变则a改变,right改变则b改变。

1.引用是指针的便捷方式

(1)分析C语言传址调用的不便:

不便一:传值调用只改变形参不改变实参,无法达到对数据的修改。
不便二:传几级指针就需要几级的解引用,如果指针的级数和解引用的级数不匹配,则会出错,而多级指针很容易出现不匹配的错误。

以顺序表传头节点指针为例:
节点的内容存放在结构体SLTNode中 -> 通过结构体指针访问节点 -> 头节点指针是一个一级结构体指针 -> 传参时为了能进入指针内部,需要传一级指针的地址,即二级指针 -> 接收时用二级指针接收

错误的调用方式:
初识C++_第30张图片

初识C++_第31张图片
phead是plist的拷贝,形参的改变不影响实参,phead=newnode无法让plist也等于newnode。

正确调用方法:
在这里插入图片描述
初识C++_第32张图片
只有通过取plist的地址,再对该地址解引用,才能让phead的值改变。

(2)用C++的引用解决不便
插入新知识:typedef结构体指针名

当对结构体重命名时,如果名称前有*符号,则该名称代表指向该结构体变量的指针名称。比如:
初识C++_第33张图片
PSLTNode是结构体指针名,PSLTNode = &SLTNode。

— — — — — — — — —分割线 — — — — — — — — —

使用引用方式,传参时传结构体指针名,接收参数时用一个别名接收,就可大大简化传地址的不便:
初识C++_第34张图片
节点名称:STLNode
节点指针:STLNode* 。STLNode*=PSTLNode
(直接在节点的基础上对结点指针重命名)
在之前的基础上有两方面的改动:
1.将所有STLNode*替换成PSTLNode
2.用引用的方式将结点指针类型的变量起别名为phead,即用&PSTLNode phead的方式接收传来的参数。

2.指针不可被引用替代的方面

(1)引用必须初始化,指针可以是NULL

引用必须初始化,就是说明起的是谁的别名。不能人还没有呢就先来个别名。

(2)引用不可以改变指向,指针可以

初识C++_第35张图片
值变,地址不变,说明没有改变c的指向,只改变了c里面的值。只能给A增加或更换别名,不能把A的别名拿着说这是B的别名。

由于C++无法改变指向即地址,因此数据结构的某些部位如链表还是要用指针。比如在链表中插入一个节点,此时需要改变指针的指向,无法用引用的方式完成。

3.总结引用和指针的异同点

(1)相同点

引用和指针的底层都开辟空间了。
初识C++_第36张图片

(2)不同点

1.引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求。
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。即引用不可以改变指向,指针可以。
4. 没有NULL引用,但有NULL指针。
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
7. 有多级指针,但是没有多级引用。
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
9. 引用比指针使用起来相对更安全。

☀️三、引用作参数、引用作返回值

1.做参数,方便好理解

初识C++_第37张图片

2.做返回值

(1)引用返回的风险

引用返回的本质是野指针:
返回值原本在函数的栈帧中有一席之地,但函数运行完被销毁了,返回值所在的空间也被销毁了(被销毁不是说空间消失了或不能用了,意思是这片空间不受管辖,可以随意被分配给其他地方装载其他值)。引用返回,返回的是变量n的别名,通过该返回值的别名找到这块不受管辖的空间,就如同野指针。
对比传值返回:函数调用结束,返回的变量也跟着销毁,所以实际返回的不是变量本身,而是变量的拷贝。
初识C++_第38张图片
在这里插入图片描述
说明在vs下,销毁栈帧不会换成随机值,在被其他值覆盖前保持之前函数在此处放的值。其他编译器下不一定。

(2)引用返回的正确用法

在被返回的值前加static,被static修饰的局部变量只会被初始化一次。
①此时的ret已经和Add(1,2)的返回值绑定:

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

在这里插入图片描述

②在(1)的 基础上实现返回值的变动,此时ret和c这块空间内放的任何数据都绑定:

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

在这里插入图片描述

☀️四、传值、传引用效率比较

1.传值调用&传引用调用

测试对比传值调用和传引用调用的时间差异:

#include 
using std::cout;
using std::endl;
#include 
struct A { int a[10000]; };
void TestFunc1(struct A a) {}
void TestFunc2(struct A& a) {}
//也可以不加struct写成:void TestFunc1(A a) {}
//                    void TestFunc2(A& a) {}
void TestRefAndValue()
{
	struct 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;
}

在这里插入图片描述
结论:传引用调用效率高。

2.传值返回&传引用返回

#include 
using std::cout;
using std::endl;
#include 
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;
}

在这里插入图片描述

结论:传引用返回效率高得多。

3.引用返回可做左值,方便修改返回对象

传值返回时,返回的是一个拷贝的值,这个值只能放在等号右边,即必须要用一个变量接收这个返回值;引用返回,返回的是下标为pos的那个位置的别名,可以充当等好的左边,可以被改变。因此对这个位置赋值或修改是很方便的(比指针方便,指针还要解引用呢)。
初识C++_第39张图片
初识C++_第40张图片

☀️五、常引用

1.常引用概念:

简单来说就是给变量或者常量取带有常属性的别名,具有了常属性就不可被修改了。
一个变量前加上const后就具有了常属性。从变量到常量属于权限的缩小;从常量到常量属于权限的平移;从常量到变量属于权限的放大。

复习一下宏:

1.概念:宏就是一种单纯的替换,没有类型,可以替换函数表达式、变量常量等;宏后面不可加分号;宏会被替换成表达式。
2.注:如果宏用来替换一个表达式,则对每一项运算对象都加括号,防止计算对象本身就是表达式。例如:
初识C++_第41张图片
如果宏定义的ADD是:ADD(x,y) (x+y),则容易出现运算符优先级的错误。

2.两种常引用操作

(1)对无常属性变量常引用
int a = 10;
const int& ra = a;

ra是a的别名,同时ra具有常属性,从a到ra属于权限的缩小。

(2)对有常属性变量常引用
const int a = 10;
const int& ra = a;

ra是a的别名,a本身就具有常属性,ra也有常属性,从a到ra属于权限的平移。

❌易犯错误:给具有常属性的变量起没有常属性的别名,属于放大权限:

const int a = 10;
int& ra = a;
(3)对常量常引用
const int& b = 10;

10本身是一个常量,b是10的别名,因此也必须有常属性,从10到b属于权限的平移。

❌易犯错误:给常量起没有常属性的别名,属于放大权限:

int& b = 10;
(4)别名的数据类型不同
double d = 12.34;
const int& rd = d;

整型可以隐式转换成double类型,内部过程是整型的i先存进一个临时变量中转换成double,再存储到double类型的变量j中,由于临时变量具有常属性,因此给double类型的引用加上const后,就可以存进整型的i了。简单来说,类型不一样,进行转换时,加个const就可以了。

❌易犯错误:

double d = 12.34;
int& rd = d;

内联函数

☀️内联函数概念:

以inline修饰的函数叫做内联函数,在编译期间编译器会用函数体替换函数的调用。相当于C语言的宏。

注:C++中替代宏的技术:
1.常量定义,换用const enum
2. 短小函数定义,换用内联函数

☀️内联函数特性

1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
缺陷:可能会使目标文件变大。
优势:没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

☀️使用内联函数注意事项

内联只适合小函数(10行以内),当大函数用内联时(假设大函数100行,一共调用量10000次),则展开指令合计100*10000行,不展开指令合计100+10000行。内联函数会影响可执行程序大小,影响更新的大小和时间,无条件地使用内联函数会让程序很庞大。

auto关键字

变量前加上auto关键字,可以识别出这个是什么类型的变量,从而替代了原先的准确变量类型。
初识C++_第42张图片
在这里插入图片描述
typeid是打印类型函数,typeid(变量名).name()。

注:如果想让auto推导出引用的话,必须写成auto&,此时推导出的类型是int在这里插入图片描述

☀️auto真正的意义:

1.代替长类型,如

在这里插入图片描述
变量类型是vector< s t r i n g string string>: :iterator,太长了,用auto简化:在这里插入图片描述

注意:
1.用auto替代类型名的变量必须要在右边给值初始化 :
在这里插入图片描述
2.auto不可以作为函数返回值, 因为如果不调用该函数,或者无返回值,此时编译器就不知道推导什么了:
初识C++_第43张图片

3.auto不可以声明数组:
初识C++_第44张图片

2.用于遍历数组

(1)C语言通过下标或指针来遍历数组:

初识C++_第45张图片

(2)C++基于数组遍历:

依次取数组中的数据赋值给e,自动判断结束,自动++往后走。也可以用int,但auto很方便,适用于各个类型的数组。
初识C++_第46张图片
注:e是对数组数据的拷贝,对e的修改不影响实际数组中的数据,即下面的写法不起作用:
初识C++_第47张图片

如果想要实现堆数组中数据的改变,则需要用引用的方式让e数组数据共用同一块空间:
初识C++_第48张图片
注:错误写法:
初识C++_第49张图片
此时不可以这样写,因为传参传进来的是指针不是数组(以array[ ]接收到的参数是指针,相当于(int* array)),即array在这个函数中是一个指针名而不是数组名,for循环写法只能基于数组。

NULL和nullptr

☀️NULL:

NULL本质上是一个宏,有些极端情况下被替换成数字0,从而引发麻烦,如果硬要将NULL按照指针方式来使用,必须对其进行强转(void ∗ * )。
初识C++_第50张图片

☀️nullptr:

C++中空指针换成nullptr,能用NULL的情况下都能用nullptr,而且还不会被替换成0。nullptr等价于(void ∗ * )NULL。

证明:

当传参为NULL时:
初识C++_第51张图片
(注:函数用于接受参数的变量只有类型无变量名,但不会报错,因为程序不用传进来的值,只要传一个整型数据就行)
初识C++_第52张图片
证明NULL被当成数字而不是指针。

当传参为nullptr时:
在这里插入图片描述
初识C++_第53张图片
证明nullptr就是指针类型。

你可能感兴趣的:(c++,开发语言)