初始C++入门(2)

缺省参数

缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时,如果没有指定实
参则采用该形参的缺省值,否则使用指定的实参。
有句俗话叫,做人不能像缺省函数。
#include
using namespace std;

void Func(int n=0)
{
	cout << n << endl;
}

int main()
{
	Func();
	Func(10);

}

初始C++入门(2)_第1张图片

那么缺省函数有什么作用呢? 

我们可以再次拿栈来举例,但我们不知道要往栈插入多少数据时,一般是#define数组大小,但这样不太好,因为数组大小给大了 浪费空间 数组大小给小了 空间不够,所以一般我们都是在堆上malloc开辟与指针结点大小一样的空间,再根据容量capacity相等否size数组大小进行扩容,但今天我们学了缺省函数以后就不用这样了!可以更加方便的插入一定的数据。

Stack.h
#pragma once
#include
#include
#include

typedef struct Stack
{
	int* a;
	int top;
	int capacity;
}ST;
void STInit(ST* ps,int n=4);
void STPush(ST* ps,int x);


test.cpp
#include"Stack.h"
void STInit(ST* ps, int n)
{
	//ps->a=NULL
	ps->a = (int*)malloc(sizeof(int) * n);
	ps->capacity = ps->top = 0;
}
void STPush(ST* ps, int x)
{
	//...
}
using namespace std;


int main()
{
	ST st;
	STInit(&st,100);
	for(size_t i=0;i<100;i++)
	{
		STPush(&st, i);
	}
	return 0;

}

但我们给缺省值时,我们可以根据自己的插入数据的个数来调差缺省值,比如我这边假设插入100个数据,就可以把n改成100,因为当我们给缺省值指定实参时,它会调用指定实参。

但缺省参数不能在函数的声明和定义同时给,会报错,所以祖师爷一般规定在声明给缺省参数值即可。缺省值必须是常量或者全局变量。

Stack.h
#pragma once
#include
#include
#include

typedef struct Stack
{
	int* a;
	int top;
	int capacity;
}ST;
void STInit(ST* ps,int n=4);
void STPush(ST* ps,int x);


test.cpp
#include"Stack.h"
void STInit(ST* ps, int n)
{
	//ps->a=NULL
	ps->a = (int*)malloc(sizeof(int) * n);
	ps->capacity = ps->top = 0;
}
void STPush(ST* ps, int x)
{
	//...
}
using namespace std;


int main()
{
	ST st;
	STInit(&st,100);
	for(size_t i=0;i<100;i++)
	{
		STPush(&st, i);
	}
	return 0;

}

全缺省函数

#include"Stack.h"

using namespace std;

void Func(int a=20,int b=30,int c=40)
{
	cout << "a= "<初始C++入门(2)_第2张图片

半缺省函数

void Func(int a=20,int b=30,int c)
{
	cout << "a= "<初始C++入门(2)_第3张图片

void Func(int a=20,int b,int c=30)
{
	cout << "a= "<int ADD(int left, int right)
{
	cout << "int ADD(int left, int right)" << endl;
	return left + right;
}
int ADD(double left, double right)
{
	cout << "int ADD(double left, double right)" << endl;
	return left + right;
}

int main()
{
	ADD(1, 2);
	ADD(1.23, 1.34);


	
	return 0;

}

初始C++入门(2)_第4张图片

为什么这样写会报红呢?

因为double与 int 是可以完成隐式类型转换的  当这两个函数同时存在时会存在歧义,不知道调用哪个,当我们注释掉一个函数就可以调用了。

初始C++入门(2)_第5张图片

 参数不同

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

两个构成函数重载,但在我们不传参时调用会存在歧义

当我们不给指定实参时,编译器不知道调用哪个,因为不传参值与指定实参值相同。

改成这样就ok了。

初始C++入门(2)_第6张图片c

参数类型顺序不同 

void f(int a,char b)
{
	cout << "int f(int a,char b)" << endl;
	cout << a  <

返回值有时候不能不同

初始C++入门(2)_第7张图片

一样会存在歧义,因为double和int能进行隐式类型转换。 

那为什么C++支持函数重载呢?而C语言不支持呢?

当我们有Func.h   func.cpp  test.cpp 三个文件时

在C语言编译链接中我们学过,一个程序要运行起来要经历4个阶段:预处理 编译  汇编 链接

预处理:头文件展开/宏替换/条件编译/去掉注释   Func.i  test.i

编译:检查语法/生成汇编代码         Func.s  test.s

汇编:转换成二进制的机器码         Func.o test.o

链接:合并到一起,链接一些函数还没有确定的等等    a.out

由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字。但目前本人才疏学浅,只能简单演示下过程和原理,详细一步一步的调试等我再试炼试炼后,我再进行补充。

Linux下简单演示下过程和原理

初始C++入门(2)_第8张图片

初始C++入门(2)_第9张图片

初始C++入门(2)_第10张图片

在Linux中,在采用gcc(编译C语言文件)编译完成后,函数名修饰并没有发生改变。

初始C++入门(2)_第11张图片

采用g++(编译C++文件)编译

初始C++入门(2)_第12张图片初始C++入门(2)_第13张图片

Linux函数名修饰规则
_Z   函数名字符个数   函数名   参数首字母
linux 下,采用 g++ 编译完成后,函数名字的修饰发生改变,编译器将函数参 数类型信息添加到修改后的名字中。
所以C语言在链接函数地址中,就会用函数名去找,但C语言不存在同名函数 不能构成函数重载。
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。
C++ 是通过函数修 饰规则来区分
假设C++有两个函数名相同 参数类型不同的构成函数重载时
初始C++入门(2)_第14张图片
初始C++入门(2)_第15张图片
初始C++入门(2)_第16张图片
如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分

&引用

引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空
间,它和它引用的变量 共用同一块内存空间。
比如鲁迅又名周树人 李逵又名黑旋风
注意:引用类型必须和引用实体是同种类型的。
初始C++入门(2)_第17张图片

但这种引用的意义不大,引用最大的意义是做参数。 

初始C++入门(2)_第18张图片 初始C++入门(2)_第19张图片

那么引用和引用实体它们的地址相同不相同呢?

初始C++入门(2)_第20张图片

结果可以发现地址是相同的,因为引用没有开辟新的空间,只是引用实体的别名罢了。

初始C++入门(2)_第21张图片

 引用必须初始化

初始C++入门(2)_第22张图片

初始C++入门(2)_第23张图片

初始C++入门(2)_第24张图片

初始C++入门(2)_第25张图片

初始C++入门(2)_第26张图片

 总结

1. 引用在 定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体

引用的价值

1.Swap交换函数

C语言写法

一般用传址给Swap函数,然后Swap函数开辟函数栈帧,进行数字之间的交换,再销毁,再返回给寄存器。

初始C++入门(2)_第27张图片

初始C++入门(2)_第28张图片

C++写法

直接使用引用,使用别名直接对实参a b本身修改,又不用开辟函数栈帧,也不用传址 提高效率

初始C++入门(2)_第29张图片

初始C++入门(2)_第30张图片

教科书上链表的写法

一般C语言写法

我们为了修改一级指针就需要进行传址并用二级指针接受才能进行修改一级指针指向的内容

typedef struct SListNode
{
	struct ListNode* next;
	int val;
}SLNode;

void PushBack(SLNode** phead, int x)
{
	if (*phead == NULL)
	{
		*phead = newode;
	}
	else
	{
		tail->next = newnode;
	}
}

int main()
{
    SLNode *plist = NULL;
	PushBack(&plist, 1);
	PushBack(&plist, 2);

    return 0;
}

教科书上的写法

typedef struct SListNode
{
	struct ListNode* next;
	int val;
}SLNode,*PListNode;

void PushBack(PListNode& phead, int x)
{
	if (phead == NULL)
	{
		phead = newonode;
	}
	else
	{
		tail->next = newnode;
	}
}

int main()
{

    SLNode plist = NULL;
	PushBack(plist, 1);
	PushBack(plist, 2);
    return 0;
}

实际第一个typedef SLNode是把SListNode修改成了 SLNode

第二个typedef 是把struct ListNode的节点指针修改成了 *PlistNode

相当于C++中的引用 PlistNode& phead 。很多同学在还没学习的C++的时候,观察数据结构教材书或者自己网上买的数据结构中的链表这样写,不明白到底是什么意思,其实就是C++的引用。

但并不推荐这样写 因为不够直观 而且很多C语言学习者并没有学习过C++,导致学习者的会更加混淆指针的理解与降低学习兴趣。

推荐写法

typedef struct SListNode
{
	struct ListNode* next;
	int val;
}SLNode;

void PushBack(SLNode& phead, int x)
{
	if (phead == NULL)
	{
		phead = newonode;
	}
	else
	{
		tail->next = newnode;
	}
}

int main()
{
    SLNode *plist = NULL;
	PushBack(plist, 1);
	PushBack(plist, 2);


    returm 0;
}

引用做返回值

初始C++入门(2)_第31张图片

初始C++入门(2)_第32张图片

初始C++入门(2)_第33张图片

初始C++入门(2)_第34张图片

为什么会出现这种情况呢?

虽然做引用返回值结果是正常的,但引用返回值返回两次,第二次结果就不对了,第三次又返回了正常,这又是为什么呢?

当用引用做返回值返回n时,相当于是n的别名,当count函数的函数栈帧销毁,n也会随着销毁,那么返回的时候n的值就是随机值。

虽然说vs2019保留了n的值并且屏幕输出了1(VS优化和处理比较好) 但其他编译器就不一定了。

我们再来观察一个函数

初始C++入门(2)_第35张图片

初始C++入门(2)_第36张图片

虽然引用做返回值的结果是正常的

但ret的值依旧取决于编译器对这块调用函数栈帧的销毁

所以第一个ret 要么是3 or 随机值 第二个ret同理 要么是7 or 随机值  (取决于编译器)

结论:当出了作用域,返回对象就销毁了,不能使用引用返回,否则结果返回的值是不确定的

所以只要出了作用域并不会销毁的就可以使用引用返回

比如静态区的静态变量和全局变量 堆区上开辟的空间。

时间一去不复返,但空间可以重复利用,当Func1的函数栈帧销毁以后,Func2再去建立函数栈帧利用空间是相同的。

初始C++入门(2)_第37张图片

初始C++入门(2)_第38张图片

引用做返回值——静态变量

初始C++入门(2)_第39张图片

因为静态变量出了Add函数作用域还存在and不会被销毁,且静态变量只会被初始化一次

所以当调用第一次Add函数值为3 第二次调用的值不变还是3

初始C++入门(2)_第40张图片

静态变量只初始化一次 所以第一次调用n=3,第二次调用Add 代码会一步一步执行就会被改变

因为相当于赋值 值为7。

传值和传引用效率的比较


做参数比较初始C++入门(2)_第41张图片
做返回值比较

初始C++入门(2)_第42张图片

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大

链表修改pos位置的值

初始C++入门(2)_第43张图片

为什么这里可以用引用做返回值,因为出了作用域链表里面的ps->a不会销毁,因为是在堆上空间开辟的,没有free是不会销毁的,因此可以在链表修改pos的值时使用引用返回修改对象。

引用的常引用(权限问题)

初始C++入门(2)_第44张图片

 初始C++入门(2)_第45张图片初始C++入门(2)_第46张图片初始C++入门(2)_第47张图片

引用可以权限的平移 权限的缩小 但不能权限的放大。

开头我讲的引用和引用实体类型必须相同就是这个道理

因为当double&d=i时,int 和double会存在隐式类型转换,产生一个临时变量,而临时变量具有常属性,不能被修改,所以double&d=i是错误的。但加上const造成权限的缩小就可以引用。

引用和指针的区别

初始C++入门(2)_第48张图片

可以从反汇编看出来 虽说引用语法不开空间 但底层是开发了空间的。

所以说语法与底层相反的 在底层实现上引用实际是有空间的,因为引用是按照指针方式来实现的。

初始C++入门(2)_第49张图片

内联函数

在讲内联函数之间,需要先提一下#define宏,因为内联函数继承了宏的优点且优化了宏的缺点。

宏的缺点:1.不能调试 2.容易出错,语法细节多(比如define ADD函数时必须要加双括号,否则会出现优先级问题,导致数值进行错误相加) 3.没有类型安全的检查

什么是内联函数?

inline 修饰 的函数叫做内联函数, 编译时 C++ 编译器会在 调用内联函数的地方展开 ,没有函数调
用建立栈帧的开销,内联函数提升程序运行的效率。
初始C++入门(2)_第50张图片

我们通过汇编来观察内联函数是否展开

但是debug环境下一般是写完代码然后去寻找错误的,所以在debug去查看汇编的时候,默认情况是没有在调用的时候去展开的 默认情况下确实没有展开 还是有call 说明还是会建立栈帧。

初始C++入门(2)_第51张图片

 要在debug查看内联函数展开要进行以下设置

点击项目名称右键查看属性

初始C++入门(2)_第52张图片

初始C++入门(2)_第53张图片初始C++入门(2)_第54张图片

初始C++入门(2)_第55张图片

初始C++入门(2)_第56张图片

当设置完成后再次调试打开反汇编查看

改完属性发现call消失了 说明了内联函数展开了并且不用建立函数栈帧空间。

初始C++入门(2)_第57张图片

内联函数声明和定义不能分离

初始C++入门(2)_第58张图片

初始C++入门(2)_第59张图片

初始C++入门(2)_第60张图片

最好的方法就是声明和定义绑定

初始C++入门(2)_第61张图片

初始C++入门(2)_第62张图片

初始C++入门(2)_第63张图片

inline函数的特性

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

《C++prime》第五版关于inline的建议:
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。

这个代码行数是不固定的 取决于编译器。

初始C++入门(2)_第64张图片

初始C++入门(2)_第65张图片

这个时候代码就没有展开了。

这个内联函数 假设有100行 func函数 调用10000次

展开是100*10000次  不展开是100+10000(10个func函数+10000call func)

影响可执行程序的大小 比如游戏更新 安卓手机比苹果手机慢

比如更新一次版本,安卓用户:王者荣耀需要更新1GB多 苹果用户:王者荣耀只需要更新400多MB即可。苹果的底层原理是比较优越的。

范围的for循环

一般写数组

初始C++入门(2)_第66张图片

初始C++入门(2)_第67张图片

对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因
C++11 中引入了基于范围的 for 循环。 for 循环后的括号由冒号 分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围
初始C++入门(2)_第68张图片

初始C++入门(2)_第69张图片

初始C++入门(2)_第70张图片

 初始C++入门(2)_第71张图片

auto关键字

auto是一个关键字,可以自动识别变量的类型,也能代替特别长的类型名称

初始C++入门(2)_第72张图片

初始C++入门(2)_第73张图片

auto 意义定义对象时,类型较长,用它比较方便。

初始C++入门(2)_第74张图片

初始C++入门(2)_第75张图片

初始C++入门(2)_第76张图片

auto关键字暂且了解这么点即可。

指针空值nullptr

C语言之父发明C语言语法的时候,犯了一个错误就是把NULL 定义成宏#define NULL 0

C++之父为了弥补这个语法错误,就发明了C++中nullptr指针空值

初始C++入门(2)_第77张图片初始C++入门(2)_第78张图片

我们本想通过f(NULL)调用f(int*),但结果却是两个int。

程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0 ,因此与程序的
初衷相悖。
在C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器
默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void
*)0

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

初始C++入门(2)_第79张图片

为了不与程序出现冲突,C++之父引进了nullptr关键字。

nullptr注意事项

1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr C++11 作为新关键字引入
2. 在 C++11 中, sizeof(nullptr) sizeof((void*)0) 所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr

你可能感兴趣的:(c++,开发语言,数据结构,c语言,链表)