缺省参数
#include<iostream>
using namespace std;
void Func(int n=0)
{
cout << n << endl;
}
int main()
{
Func();
Func(10);
}
那么缺省函数有什么作用呢?
我们可以再次拿栈来举例,但我们不知道要往栈插入多少数据时,一般是#define数组大小,但这样不太好,因为数组大小给大了 浪费空间 数组大小给小了 空间不够,所以一般我们都是在堆上malloc开辟与指针结点大小一样的空间,再根据容量capacity相等否size数组大小进行扩容,但今天我们学了缺省函数以后就不用这样了!可以更加方便的插入一定的数据。
Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
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<stdio.h>
#include<stdlib.h>
#include<iostream>
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= "<<a<< endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl<<endl;
}
int main()
{
Func();
Func(10,2);
Func(10, 20, 30);
//Func(,2,)//不能这样给
return 0;
}
半缺省函数
void Func(int a=20,int b=30,int c)
{
cout << "a= "<<a<< endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl<<endl;
}
当我们传2个指定实参时会报红为什么呢?
因为缺省值规定必须从右往左给 而且要连续给 全缺省也一样
报红是因为编译器认为2是给C的 那么10是给谁的?
所以从左往右传参会存在歧义。
为什么是从右往左给参数?因为在函数建立栈帧时,eax和ecx压栈时的传参也是先传右再传左进行压栈的。
void Func(int a=20,int b,int c=30)
{
cout << "a= "<<a<< endl;
cout << "b= " << b << endl;
cout << "c= " << c << endl<<endl;
}
这样也是一样的道理,必须从右往左传,必须连续,要不然一样会报红。
函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!
提到中国足球,不得不提到昨天的国足1:2遭乌兹别克斯坦逆转...
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
函数重载可以参数类型不同 函数名相同 返回值可同可不同
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;
}
为什么这样写会报红呢?
因为double与 int 是可以完成隐式类型转换的 当这两个函数同时存在时会存在歧义,不知道调用哪个,当我们注释掉一个函数就可以调用了。
参数不同
int f()
{
cout << "f()" << endl;
}
int f(int a = 0)
{
cout << "f(int a)" << endl;
}
两个构成函数重载,但在我们不传参时调用会存在歧义
当我们不给指定实参时,编译器不知道调用哪个,因为不传参值与指定实参值相同。
改成这样就ok了。
c
参数类型顺序不同
void f(int a,char b)
{
cout << "int f(int a,char b)" << endl;
cout << a <<endl<< b << endl;
}
void f(char b,int a)
{
cout << "int f(char a,int b)" << endl;
cout << b << endl<<a << endl;
}
int main()
{
f(1 ,'x');
f('x' ,3);
return 0;
}
返回值有时候不能不同
一样会存在歧义,因为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下简单演示下过程和原理
在Linux中,在采用gcc(编译C语言文件)编译完成后,函数名修饰并没有发生改变。
采用g++(编译C++文件)编译
&引用
但这种引用的意义不大,引用最大的意义是做参数。
那么引用和引用实体它们的地址相同不相同呢?
结果可以发现地址是相同的,因为引用没有开辟新的空间,只是引用实体的别名罢了。
引用必须初始化
总结
引用的价值
1.Swap交换函数
C语言写法
一般用传址给Swap函数,然后Swap函数开辟函数栈帧,进行数字之间的交换,再销毁,再返回给寄存器。
C++写法
直接使用引用,使用别名直接对实参a b本身修改,又不用开辟函数栈帧,也不用传址 提高效率
教科书上链表的写法
一般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;
}
引用做返回值
为什么会出现这种情况呢?
虽然做引用返回值结果是正常的,但引用返回值返回两次,第二次结果就不对了,第三次又返回了正常,这又是为什么呢?
当用引用做返回值返回n时,相当于是n的别名,当count函数的函数栈帧销毁,n也会随着销毁,那么返回的时候n的值就是随机值。
虽然说vs2019保留了n的值并且屏幕输出了1(VS优化和处理比较好) 但其他编译器就不一定了。
我们再来观察一个函数
虽然引用做返回值的结果是正常的
但ret的值依旧取决于编译器对这块调用函数栈帧的销毁
所以第一个ret 要么是3 or 随机值 第二个ret同理 要么是7 or 随机值 (取决于编译器)
结论:当出了作用域,返回对象就销毁了,不能使用引用返回,否则结果返回的值是不确定的
所以只要出了作用域并不会销毁的就可以使用引用返回
比如静态区的静态变量和全局变量 堆区上开辟的空间。
时间一去不复返,但空间可以重复利用,当Func1的函数栈帧销毁以后,Func2再去建立函数栈帧利用空间是相同的。
引用做返回值——静态变量
因为静态变量出了Add函数作用域还存在and不会被销毁,且静态变量只会被初始化一次
所以当调用第一次Add函数值为3 第二次调用的值不变还是3
静态变量只初始化一次 所以第一次调用n=3,第二次调用Add 代码会一步一步执行就会被改变
因为相当于赋值 值为7。
传值和传引用效率的比较
做参数比较
做返回值比较
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
链表修改pos位置的值
为什么这里可以用引用做返回值,因为出了作用域链表里面的ps->a不会销毁,因为是在堆上空间开辟的,没有free是不会销毁的,因此可以在链表修改pos的值时使用引用返回修改对象。
引用的常引用(权限问题)
引用可以权限的平移 权限的缩小 但不能权限的放大。
开头我讲的引用和引用实体类型必须相同就是这个道理
因为当double&d=i时,int 和double会存在隐式类型转换,产生一个临时变量,而临时变量具有常属性,不能被修改,所以double&d=i是错误的。但加上const造成权限的缩小就可以引用。
引用和指针的区别
可以从反汇编看出来 虽说引用语法不开空间 但底层是开发了空间的。
所以说语法与底层相反的 在底层实现上引用实际是有空间的,因为引用是按照指针方式来实现的。
内联函数
在讲内联函数之间,需要先提一下#define宏,因为内联函数继承了宏的优点且优化了宏的缺点。
宏的缺点:1.不能调试 2.容易出错,语法细节多(比如define ADD函数时必须要加双括号,否则会出现优先级问题,导致数值进行错误相加) 3.没有类型安全的检查
什么是内联函数?
我们通过汇编来观察内联函数是否展开
但是debug环境下一般是写完代码然后去寻找错误的,所以在debug去查看汇编的时候,默认情况是没有在调用的时候去展开的 默认情况下确实没有展开 还是有call 说明还是会建立栈帧。
要在debug查看内联函数展开要进行以下设置
点击项目名称右键查看属性
当设置完成后再次调试打开反汇编查看
改完属性发现call消失了 说明了内联函数展开了并且不用建立函数栈帧空间。
内联函数声明和定义不能分离
最好的方法就是声明和定义绑定
inline函数的特性
《C++prime》第五版关于inline的建议:
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联地展开。
这个代码行数是不固定的 取决于编译器。
这个时候代码就没有展开了。
这个内联函数 假设有100行 func函数 调用10000次
展开是100*10000次 不展开是100+10000(10个func函数+10000call func)
影响可执行程序的大小 比如游戏更新 安卓手机比苹果手机慢
比如更新一次版本,安卓用户:王者荣耀需要更新1GB多 苹果用户:王者荣耀只需要更新400多MB即可。苹果的底层原理是比较优越的。
范围的for循环
一般写数组
auto关键字
auto是一个关键字,可以自动识别变量的类型,也能代替特别长的类型名称
auto 意义定义对象时,类型较长,用它比较方便。
auto关键字暂且了解这么点即可。
指针空值nullptr
C语言之父发明C语言语法的时候,犯了一个错误就是把NULL 定义成宏#define NULL 0
C++之父为了弥补这个语法错误,就发明了C++中nullptr指针空值
我们本想通过f(NULL)调用f(int*),但结果却是两个int。
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
为了不与程序出现冲突,C++之父引进了nullptr关键字。