• 初始C++入门(2)


    缺省参数

    缺省参数是 声明或定义函数时 为函数的 参数指定一个缺省值 。在调用该函数时,如果没有指定实
    参则采用该形参的缺省值,否则使用指定的实参。
    有句俗话叫,做人不能像缺省函数。
    1. #include
    2. using namespace std;
    3. void Func(int n=0)
    4. {
    5. cout << n << endl;
    6. }
    7. int main()
    8. {
    9. Func();
    10. Func(10);
    11. }

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

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

    1. Stack.h
    2. #pragma once
    3. #include
    4. #include
    5. #include
    6. typedef struct Stack
    7. {
    8. int* a;
    9. int top;
    10. int capacity;
    11. }ST;
    12. void STInit(ST* ps,int n=4);
    13. void STPush(ST* ps,int x);
    14. test.cpp
    15. #include"Stack.h"
    16. void STInit(ST* ps, int n)
    17. {
    18. //ps->a=NULL
    19. ps->a = (int*)malloc(sizeof(int) * n);
    20. ps->capacity = ps->top = 0;
    21. }
    22. void STPush(ST* ps, int x)
    23. {
    24. //...
    25. }
    26. using namespace std;
    27. int main()
    28. {
    29. ST st;
    30. STInit(&st,100);
    31. for(size_t i=0;i<100;i++)
    32. {
    33. STPush(&st, i);
    34. }
    35. return 0;
    36. }

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

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

    1. Stack.h
    2. #pragma once
    3. #include
    4. #include
    5. #include
    6. typedef struct Stack
    7. {
    8. int* a;
    9. int top;
    10. int capacity;
    11. }ST;
    12. void STInit(ST* ps,int n=4);
    13. void STPush(ST* ps,int x);
    14. test.cpp
    15. #include"Stack.h"
    16. void STInit(ST* ps, int n)
    17. {
    18. //ps->a=NULL
    19. ps->a = (int*)malloc(sizeof(int) * n);
    20. ps->capacity = ps->top = 0;
    21. }
    22. void STPush(ST* ps, int x)
    23. {
    24. //...
    25. }
    26. using namespace std;
    27. int main()
    28. {
    29. ST st;
    30. STInit(&st,100);
    31. for(size_t i=0;i<100;i++)
    32. {
    33. STPush(&st, i);
    34. }
    35. return 0;
    36. }

    全缺省函数

    1. #include"Stack.h"
    2. using namespace std;
    3. void Func(int a=20,int b=30,int c=40)
    4. {
    5. cout << "a= "<
    6. cout << "b= " << b << endl;
    7. cout << "c= " << c << endl<
    8. }
    9. int main()
    10. {
    11. Func();
    12. Func(10,2);
    13. Func(10, 20, 30);
    14. //Func(,2,)//不能这样给
    15. return 0;
    16. }

    半缺省函数

    1. void Func(int a=20,int b=30,int c)
    2. {
    3. cout << "a= "<
    4. cout << "b= " << b << endl;
    5. cout << "c= " << c << endl<
    6. }

    当我们传2个指定实参时会报红为什么呢?

     因为缺省值规定必须从右往左给 而且要连续给 全缺省也一样

    报红是因为编译器认为2是给C的 那么10是给谁的?

    所以从左往右传参会存在歧义。

    为什么是从右往左给参数?因为在函数建立栈帧时,eax和ecx压栈时的传参也是先传右再传左进行压栈的

    1. void Func(int a=20,int b,int c=30)
    2. {
    3. cout << "a= "<
    4. cout << "b= " << b << endl;
    5. cout << "c= " << c << endl<
    6. }

    这样也是一样的道理,必须从右往左传,必须连续,要不然一样会报红。

    函数重载

    自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
    比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!

    提到中国足球,不得不提到昨天的国足1:2遭乌兹别克斯坦逆转...

    函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。 

    函数重载可以参数类型不同 函数名相同 返回值可同可不同

    1. int ADD(int left, int right)
    2. {
    3. cout << "int ADD(int left, int right)" << endl;
    4. return left + right;
    5. }
    6. int ADD(double left, double right)
    7. {
    8. cout << "int ADD(double left, double right)" << endl;
    9. return left + right;
    10. }
    11. int main()
    12. {
    13. ADD(1, 2);
    14. ADD(1.23, 1.34);
    15. return 0;
    16. }

    为什么这样写会报红呢?

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

     参数不同

    1. int f()
    2. {
    3. cout << "f()" << endl;
    4. }
    5. int f(int a = 0)
    6. {
    7. cout << "f(int a)" << endl;
    8. }

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

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

    改成这样就ok了。

    c

    参数类型顺序不同 

    1. void f(int a,char b)
    2. {
    3. cout << "int f(int a,char b)" << endl;
    4. cout << a <
    5. }
    6. void f(char b,int a)
    7. {
    8. cout << "int f(char a,int b)" << endl;
    9. cout << b << endl<

    返回值有时候不能不同

    一样会存在歧义,因为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++文件)编译

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

    &引用

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

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

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

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

     引用必须初始化

     总结

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

    引用的价值

    1.Swap交换函数

    C语言写法

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

    C++写法

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

    教科书上链表的写法

    一般C语言写法

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

    1. typedef struct SListNode
    2. {
    3. struct ListNode* next;
    4. int val;
    5. }SLNode;
    6. void PushBack(SLNode** phead, int x)
    7. {
    8. if (*phead == NULL)
    9. {
    10. *phead = newode;
    11. }
    12. else
    13. {
    14. tail->next = newnode;
    15. }
    16. }
    17. int main()
    18. {
    19. SLNode *plist = NULL;
    20. PushBack(&plist, 1);
    21. PushBack(&plist, 2);
    22. return 0;
    23. }

    教科书上的写法

    1. typedef struct SListNode
    2. {
    3. struct ListNode* next;
    4. int val;
    5. }SLNode,*PListNode;
    6. void PushBack(PListNode& phead, int x)
    7. {
    8. if (phead == NULL)
    9. {
    10. phead = newonode;
    11. }
    12. else
    13. {
    14. tail->next = newnode;
    15. }
    16. }
    17. int main()
    18. {
    19. SLNode plist = NULL;
    20. PushBack(plist, 1);
    21. PushBack(plist, 2);
    22. return 0;
    23. }

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

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

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

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

    推荐写法

    1. typedef struct SListNode
    2. {
    3. struct ListNode* next;
    4. int val;
    5. }SLNode;
    6. void PushBack(SLNode& phead, int x)
    7. {
    8. if (phead == NULL)
    9. {
    10. phead = newonode;
    11. }
    12. else
    13. {
    14. tail->next = newnode;
    15. }
    16. }
    17. int main()
    18. {
    19. SLNode *plist = NULL;
    20. PushBack(plist, 1);
    21. PushBack(plist, 2);
    22. returm 0;
    23. }

    引用做返回值

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

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

    当用引用做返回值返回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.没有类型安全的检查

    什么是内联函数?

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

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

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

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

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

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

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

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

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

    inline函数的特性

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

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

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

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

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

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

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

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

    范围的for循环

    一般写数组

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

     

    auto关键字

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

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

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

    指针空值nullptr

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

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

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

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

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

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

    nullptr注意事项

    1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr C++11 作为新关键字引入
    2. 在 C++11 中, sizeof(nullptr) sizeof((void*)0) 所占的字节数相同。
    3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr
  • 相关阅读:
    JDK8和JDK17共存以及切换
    计算机组成原理---第三章存储系统---半导体随机存储器
    PyTorch深度学习
    基于ECS搭建个人网盘
    Dapr Workflow构建块的.NET Demo
    大一学生《Web编程基础》HTML实例网页代码 HTML+CSS+JS 黑色横排的个人主页作品
    跨平台应用开发进阶(四十)自定义插件及引用
    Android——FloatingActionButton使用说明以及避坑
    非支配排序遗传算法NSGA
    Python算法——快速排序
  • 原文地址:https://blog.csdn.net/weixin_74795859/article/details/133885984