• C++必修:从C到C++的过渡(下)


    ✨✨ 欢迎大家来到贝蒂大讲堂✨✨

    🎈🎈养成好习惯,先赞后看哦~🎈🎈

    所属专栏:C++学习
    贝蒂的主页:Betty’s blog

    1. 缺省参数

    1.1. 缺省参数的使用

    缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

    void func(int a = 0)
    {
    	cout << a << endl;
    }
    int main()
    {
    	func();  // 没有传参时,使用参数的默认值,输出0
    	func(1); // 传参时,使用指定的实参,输出1
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    img

    1.2. 缺省参数的分类

    根据其缺省参数的个数,我们我可以将缺省参数分为全缺省半缺省。

    1.2.1. 全缺省

    每一个参数都有缺省值。

    #include
    using namespace std;
    void func(int a = 0,int b = 1,int c = 2)
    {
    	cout <<"a=" << a << endl;
    	cout << "b = " << b << endl;
    	cout << "c = " << c << endl;
    }
    int main()
    {
    	func();//不穿参数
    	func(10,20);//半传参数
    	func(10, 20, 30);//全传
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    img

    1.2.2. 半缺省

    只有一部分参数有缺省值,并且半缺省参数必须从右往左依次来给出,不能间隔着给。

    #include
    using namespace std;
    void func(int a ,int b=1,int c=2)
    {
    	cout <<"a = " << a << endl;
    	cout << "b = " << b << endl;
    	cout << "c = " << c << endl;
    }
    int main()
    {
    
    	func(10,20);//半传参数
    	cout << endl;
    	func(10, 20, 30);//全传
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    img

    1.2.3. 注意

    在使用缺省参数时,我们也要知道一些注意事项:

    1. 传参时不能间隔传参。
    void func(int a ,int b=1,int c=2)
    {
    	cout <<"a = " << a << endl;
    	cout << "b = " << b << endl;
    	cout << "c = " << c << endl;
    }
    func(,10,20)//error
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 缺省参数不能在函数声明和定义中同时出现
    //test.h
    void Func(int a = 10);//声明
    // test.cpp
    void Func(int a = 20)//定义
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 缺省值必须是常量或者全局变量。
    2. C语言不支持(编译器不支持)

    2. 函数重载

    2.1. 函数重载的定义

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

    2.2. 函数重载的分类

    2.2.1. 参数类型不同
    int Add(int a, int b)
    {
    	return a + b;
    }
    double Add(double a, double b)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.2.2. 参数个数不同
    int Add(int a, int b)
    {
    	return a + b;
    }
    int Add(int a, int b, int c)
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2.2.3. 参数类型顺序不同
    int Add(char a, int c)
    {
    	return a + c;
    }
    int Add(int a, char c)
    {
    	return a + c;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.3. 注意

    1. 返回值类型不同无法构成函数重载
    int Add(int a, int b)
    {
    	return a + b;
    }
    double Add(int a,int b)//error
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 缺省值不同也不能构成函数重载
    int Add(int a=1, int b=20)
    {
    	return a + b;
    }
    int Add(int a=1, int b=2)//error
    {
    	return a + b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.4. 函数名修饰规则

    为什么返回值不同,缺省值不同就不能构成函数重载呢?这就要涉及C++的函数名修饰规则。

    我们在C语言当中学习编译与链接时就知道C/C++程序运行起来要经历的四个阶段:

    • 预处理:头文件展开、宏替换、条件编译、去掉注释,生成 .i 的文件。.h的文件直接被展开。
    • 编译: 语法检查(语法分析、语义分析、词法分析)、符号汇总、生成汇编代码,生成.s文件。
    • 汇编: 把汇编代码转换为二进制机器码,形成符号表,生成.o文件。符号表里存放定义函数的地址信息。
    • 链接: 合并目标文件、段表,符号表的合并和符号表的重定位,.o格式的目标文件合并到一起,生成.out/.exe文件。

    img

    我们在调用函数时,就需要去.o文件调用对应的函数地址,然后去调用函数。因为要区分函数重载的相同函数名的函数,所以函数名肯定要被修饰过,并且每个编译器的修饰规则都不一样。而在C语言中就没有这样的修饰规则,所以C语言不支持函数重载。

    下面我们可以看看在g++编译下的函数修饰规则:

    int Add(int a, int b)
    {
    	return a + b;
    }
    void func(int a, double b, int* p)
    {
    }
    int main()
    {
    	Add(1, 2);
    	func(1, 2, NULL);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    img

    g++的函数修饰规则相对较简单:

    _Z+函数名长度+每个参数类型

    3. 引用

    3.1. 引用的概念

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

    引用对象类型& 引用变量名(对象名) = 引用实体;

    引用类似于指针,因为指向同一块空间,所以改变引用变量引用实体也会改变。

    #include
    using namespace std;
    int main()
    {
    	int a = 1;
    	int& b = a;//引用
    	cout << &a << endl;
    	cout << &b << endl;
    	b++;
    	cout << a << endl;
    	cout << b << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    img

    3.2. 注意

    1. 引用在定义时必须初始化
    int&b;//必须初始化
    
    • 1
    1. 一个变量可以有多个引用
    int a=1;
    int&b=a;
    int&c=a;//多个引用
    
    • 1
    • 2
    • 3
    1. 引用一旦引用一个实体,再不能引用其他实体
    int a=1;
    int&b=a;
    b=2;//这时是赋值,相当于a=b=2;
    
    • 1
    • 2
    • 3

    3.3. 常引用

    我们可以通过const修饰引用来让其变为常引用。这时引用变量是不能被修改的,并且只能将常变量复杂给常引用,不能将常变量赋值给引用。也可以将变量赋值给常引用。

    #include
    using namespace std;
    int main()
    {
    	const int a = 1;//常变量
    	const int& b = a;//right
    	int& c = a;//error
    	int c = 2;
    	const int& d = c;//right
    	double pi = 3.14;
    	int& e = pi;//error
    	//pi是浮点型,赋值给整型类型会发生隐式类型
    	//这个隐式类型转换的值是个常变量
    	const int& f = pi;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    3.4. 引用的使用场景

    3.4.1. 作为函数的参数
    int swap(int& a, int& b)
    {
    	int tmp = 0;
    	tmp = a;
    	a = b;
    	b = tmp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    做参数就可以解决C语言中形参的改变无法影响实参的问题。

    3.4.2. 做函数返回值

    做函数返回值要注意,返回的值应在出了作用域不被销毁。不能可能出现于野指针类似的问题。

    int& Count()
    {
    	static int n = 0;
    	n++;
    	return n;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.5. 错误示例

    int& Add(int a, int b)
    {
    	int c = a + b;
    	return c;
    }
    int main()
    {
    	int& ret = Add(1, 2);
    	Add(3, 4);
    	cout << ret <<endl;
    	return 0;
    }//输出什么
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    img

    为什么会输出7呢?那是因为在第二次调用函数Add(3,4)时,会在原来第一次调用Add(1,2)建立栈帧的空间上建立栈帧所以返回值c的值会被重新覆盖,ret值也会发生改变。但因为这块空间出了作用域也会还给操作系统,所以具体结果也是未定义的。

    那我们应该如何修改呢?

    int Add(int a, int b)
    {
    	int c = a + b;
    	return c;
    }
    int main()
    {
    	int ret = Add(1, 2);
    	Add(3, 4);
    	cout << ret << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    传值返回在函数栈帧销毁前,会先将返回值拷贝放在寄存器中

    3.6. 传值与传引用

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

    我们可以通过下列代码观察一下:

    #include
    using namespace std;
    #include 
    struct A 
    { 
    	int a[10000]; 
    };
    void TestFunc1(A a) 
    {}
    void TestFunc2(A& a) 
    {
    }
    void TestRefAndValue()
    {
    	A a;
    	// 以值作为函数参数
    	size_t begin1 = clock();
    	for (size_t i = 0; i < 100000; ++i)
    		TestFunc1(a);
    	size_t end1 = clock();
    	// 以引用作为函数参数
    	size_t begin2 = clock();
    	for (size_t i = 0; i < 100000; ++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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    img

    3.7. 引用与指针的区别

    引用的底层实现与指针其实并没有什么区别。

    int main()
    {
    	int a = 10;
    	int& ra = a;
    	ra = 20;
    	int* pa = &a;
    	*pa = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们可以通过代码的汇编观察一下:

    img

    但是引用与指针还是有些区别。

    不同点引用指针
    概念变量的别名变量的地址
    初始化必须建议
    对象引用一个后不能修改可以修改指向对象
    大小引用类型的大小在32位平台为4,64位平台为8
    多级没有多级引用有多级指针
    安全性更安全并不安全

    4. 内联函数

    在C语言中,无论宏常量还是宏函数都有易出错,无法调试等缺陷。而C++为了弥补这一缺陷,一般用constenum代替宏常量,引入了内联函数的概念代替宏函数。

    4.1. 内联函数的定义

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

    #include
    using namespace std;
    inline int Add(int x, int y)
    {
    	return x + y;
    }
    int main()
    {
    	Add(1, 2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.2. 注意

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

    因为内联函数会在调用时直接展开,如果声明与定义分离内联函数的地址根本不会进入符号表,链接时就无法找到定义的函数,就会发生链接错误。

    5. auto关键字

    5.1. auto的简介

    在C++中,随着程序越来越复杂,程序所用的类型也越来越复杂。为了简化代码,增加代码的可读性,C++11引入了自动类型推断auto。在C语言中,**auto修饰的变量,是具有自动存储器的局部变量。**但是实用性很小,所以C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

    int a = 1;
    auto b = a;//自动推断b的类型
    
    • 1
    • 2

    5.2. 注意

    1. 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
    int x = 1;
    auto a = &x;
    auto* b = &x;
    auto& c = x;
    
    • 1
    • 2
    • 3
    • 4
    1. 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
    auto a = 1, b = 2;
    auto c = 2, d = 3.14;//error
    
    • 1
    • 2
    1. auto不能作为函数的参数或者声明数组
    void TestAuto(auto a)
    {
        //auto不能推断形参的类型
    }
    
    • 1
    • 2
    • 3
    • 4

    6. 范围for

    在C++98之前,我们遍历一个数组,需要按照以下的形式:

    #include
    using namespace std;
    int main()
    {
    	int arr[] = { 1,2,3,4,5 };
    	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
    	{
    		cout << arr[i] << endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是在C++11,又引入了一种新的遍历方法——范围for。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

    #include
    using namespace std;
    int main()
    {
    	int arr[] = { 1,2,3,4,5 };
    	for (auto e : arr)
    	{
    		cout << e << endl;
    	}
    	//取数组arr的值依次赋值给e
    	//自动递增,自动判断结束
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    由于e是临时变量,所以要想改变数组的值,需要引用。

    #include
    using namespace std;
    int main()
    {
    	int arr[] = { 1,2,3,4,5 };
    	for (auto&e : arr)
    	{
    		e *= 2;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。

    7. nullptr空指针

    在C语言中,定义了一个宏NULL,在传统的C头文件(stddef.h)中,可以看到如下代码 :

    #ifndef NULL
    #ifdef __cplusplus
    #define NULL 0
    #else
    #define NULL ((void *)0)
    #endif
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由此我们知道NULL既可以代表数字0,也可以代表空指针。这种模棱两可的定义就可能引出一些问题,比如下面这段代码:

    #include
    using namespace std;
    void func(int a)
    {
    	cout << "func(int)" << endl;
    }
    void func(int*p)
    {
    	cout << "func(int*)" << endl;
    }
    //函数重载
    int main()
    {
    	func(0);
    	func(NULL);
    	func((int*)NULL);
    	return 0;//输出??
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    img
    我们的本意可能是将NULL当成一个指针,但是在默认情况下NULL被编译器当做数字0。这种问题是我们并不想看见的,所以C++11引入了nullptr来代替NULL。

  • 相关阅读:
    MySQL 增删改查进阶 — 数据库约束
    解决C# 连接MYSQL数据库查询数据时Unable to convert MySQL date/time value to System.DateTime
    第九天!玩转langchain!回调处理器!一篇学会日志+监控+流式传输!9/10
    LeetCode 0640.求解方程:过几天就看不懂了的迷惑性代码,但是是详解
    网络安全(加密技术、数字签名、证书)
    SFI立昌STD系列方案与应用
    Hive 优化建议与策略
    中国古代数学问题——鸡兔同笼
    sklearn.datasets--学习笔记
    <unordered_set、unordered_map的模拟实现>——《C++高阶》
  • 原文地址:https://blog.csdn.net/Bettysweetyaaa/article/details/138183963