C++初阶(2)C++入门基础1
C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式 等。熟悉C语言之后,对C++学习有一定的帮助。
本章节主要目标:
- 补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的。
比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。 - 为后续类和对象学习打基础。
C++初阶课程的核心就是类和对象,然后使用类和对象+模版,实现基础的一些数据结构。
在学类和对象之前,需要先学C++入门,C++的祖师爷觉得C语言很多地方设计得不好,于是开发了许多C++的小语法,去改进C语言的不足。
0. C++关键字(C98)
C++总计 63 个关键字,C语言32个关键字。
ps:下面我们只是粗略看一下C++有哪些关键字,不对关键字进行具体的讲解。后面我们学到以后再细讲。
1. 命名空间
1.1 namespace的价值
在C/C++中,变量、函数和后面要学到的类都是大量存在的。
这些变量、函数和类的名称若都存在于全局作用域中的话,则很可能会导致很多冲突。
使用命名空间的目的
- 对标识符的名称进行本地化,以避免命名冲突或名字污染。
namespace 关键字的出现就是针对这种问题的。
c语言项目类似下面程序这样的命名冲突是普遍存在的问题,C++引入namespace就是为了更好的解决这样的问题。(C的缺陷 / 不足)
#include <stdio.h?
#include <stdlib.h> //不包含这个头文件,则不会报错
int rand = 10;
int main()
{// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”printf("%d\n", rand);return 0;
}//头文件展开,里面的rand()函数和全局的rand变量——>命名冲突
即C语言中,变量不能和函数重名。
命名冲突——C的缺陷之一:存在于编码者与库、(一个大项目的)编码者之间。
C语言是解决不了这个问题的,两个都想叫rand,最终只能有一个在全局域取名为rand。
解决C语言第一个缺陷——命名冲突:同名xx不知道使用哪一个,的第一个C++语法namespace。
7.2 namespace的语法规则
(1)namespace定义
• 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{ }即可。——和结构体不一样的是,后面不需要加分号。
{}中即为命名空间的成员,可以把日常定义的变量、函数、类型封装到命名空间里面。
- 命名空间中可以定义变量/函数/类型等。
命名空间中是各种标识符的定义:变量名、函数名、类型名……
命名空间中的变量、函数、类型不会和全局冲突,只有指定才会找到。
(2)域
• namespace本质是定义出一个域(命名空间域),这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不再冲突了。
C++中域有函数局部域、全局域、命名空间域、类域;
#include <stdio.h>
#include <stdlib.h>
// 1. 正常的命名空间定义
// bit是命名空间的名字,⼀般开发中是用项⽬名字做命名空间名。
// 示例用的是bit,自己练习可以考虑用自己名字缩写,如张三:zs
namespace bit
{// 命名空间中可以定义变量/函数/类型int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}int main()
{// 这里默认是访问的是全局的rand函数指针printf("%p\n", rand);// 这里指定bit命名空间中的randprintf("%d\n", bit::rand);return 0;
}
代码编译时,遇到标识符——变量(名)、函数(名)、类型(名),编译器需要去找它的出处(定义),找不到会报错“未声明的标识符”。
编译默认查找顺序——
- 当前局部域(自留地)
- 全局域找——头文件包含的东西也在全局域(村子野地)
类比做菜摘葱,优先去自留地摘,没有再去村子野地摘。
命名空间相当于在全局域,划分出一些独立的域(命名空间域)
——将一些村子野地,划归成自家的自留地。
编译时不会到其他命名空间中去找(隔壁张大爷自留地)。
这样的话,加了命名空间,同时不包含 <stdlib.h>头文件,就会报错:errorC2065:未声明的标识符。
而指定查找域,则只会去这一个域里面找。
类比做菜摘葱,指定去张大爷地摘。
(3)域的作用
不同域可以定义同名的变量/函数/类型——域可以做到名字的隔离。
{ }括起来的都是域,全局域可以不用{ }括起来。
• 域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所以有了域隔离,名字冲突就解决了。
- 局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期。
- 命名空间域和类域不影响变量生命周期。
(命名空间域都是修饰全局的,只是把名字给隔离起来了)
域的作用是影响编译时的查找规则——先到局部域,再到全局域&展开的命名空间域。
局部优先原则: 默认优先访问局部变量。
C语言,在局部,访问同名全局变量的方法。
C++,在局部,访问同名全局变量的方法——作用域限定符(作用域解析运算符)
(4)嵌套定义
• namespace只能定义在全局,当然它还可以嵌套定义。
变量rand还是全局变量,只是封装到了bit命名空间中。
命名空间内部可以定义变量、函数、类型,还可以定义其他的命名空间。
//2. 命名空间可以嵌套
namespace bit
{// 鹏哥namespace pg{int rand = 1;int Add(int left, int right){return left + right;}}// 杭哥namespace hg{int rand = 2;int Add(int left, int right){return (left + right)*10;}}
}int main()
{printf("%d\n", bit::pg::rand);printf("%d\n", bit::hg::rand);printf("%d\n", bit::pg::Add(1, 2));printf("%d\n", bit::hg::Add(1, 2));return 0;
}
嵌套的命名空间,在使用的时候也要使用多个域作用限定符来访问。
(5)多个同名namespace
• 项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
// 多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样
// Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>namespace bit
{typedef int STDataType;typedef struct Stack{STDataType* a;int top;int capacity;}ST;void STInit(ST* ps, int n);void STDestroy(ST* ps);void STPush(ST* ps, STDataType x);void STPop(ST* ps);STDataType STTop(ST* ps);int STSize(ST* ps);bool STEmpty(ST* ps);
}// Stack.cpp
#include"Stack.h"
namespace bit
{void STInit(ST* ps, int n){assert(ps);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;}// 栈顶void STPush(ST* ps, STDataType x){assert(ps);// 满了, 扩容if (ps->top == ps->capacity){printf("扩容\n");int newcapacity = ps->capacity == 0 ? 4 : ps->capacity*2;STDataType* tmp = (STDataType*)realloc(ps->a,newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}ps->a = tmp;ps->capacity = newcapacity;}ps->a[ps->top] = x;ps->top++;}//...
}// Queue.h
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>namespace bit
{typedef int QDataType;typedef struct QueueNode{int val;struct QueueNode* next;}QNode;typedef struct Queue{QNode* phead;QNode* ptail;int size;}Queue;void QueueInit(Queue* pq);void QueueDestroy(Queue* pq);// 入队列void QueuePush(Queue* pq, QDataType x);// 出队列void QueuePop(Queue* pq);QDataType QueueFront(Queue* pq);QDataType QueueBack(Queue* pq);bool QueueEmpty(Queue* pq);int QueueSize(Queue* pq);
}// Queue.cpp
#include"Queue.h"
namespace bit
{void QueueInit(Queue* pq){assert(pq);pq->phead = NULL;pq->ptail = NULL;pq->size = 0;}// ...
}// test.cpp
#include"Queue.h"
#include"Stack.h"// 全局定义了一份单独的Stack
typedef struct Stack
{int a[10];int top;
}ST;void STInit(ST* ps){}
void STPush(ST* ps, int x){}int main()
{// 调用全局的ST st1;STInit(&st1);STPush(&st1, 1);STPush(&st1, 2);printf("%d\n", sizeof(st1));// 调用bit namespace的bit::ST st2;printf("%d\n", sizeof(st2));bit::STInit(&st2);bit::STPush(&st2, 1);bit::STPush(&st2, 2);return 0;
}
情景1:假设上述代码中,栈的队列的初始化函数,都希望叫作Init(不希望用名字来区分),那么就需要将栈和队列的代码,放入不同的命名空间中。
情景2:害怕自己的栈的STInit()和别人的冲突,就不能直接#include "Stack.h",这样会直接暴露在全局域,需要先在头文件中,把代码都放到命名空间中,才能#include "Stack.h"
一个一般的项目有几十上百个头文件,定义多少个命名空间?
(不分命名空间的话因为变量、函数、类型……的名字就得拉扯很久理不清)
一般分组合作,一个组内几个头文件共用一个命名空间。
不同文件可以定义同名的命名空间,同名的命名空间可合并。
合并后(同一个域)有同名会报错——命名冲突——1改名,2嵌套
而同一个文件也没必要搞多个命名空间,一个足矣,即都放到一起——因为多个文件都可以共用一个命名空间。
(6)标准库——标准命名空间
• C++标准库都放在一个叫 std (standard的缩写)的命名空间中。
7.3 命名空间使用(3种方法)
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以下面程序会编译报错。
#include<stdio.h>
namespace bit
{int a = 0;int b = 1;
}int main()
{// 编译报错:error C2065: “a”: 未声明的标识符printf("%d\n", a);return 0;
}
其他的例证:
原因:有两个rand,这里printf里的rand默认取库里面的rand,即函数rand(),代表一个函数指针,d%改成p%就可以打印出这个函数指针。
所以我们要使用命名空间中定义的变量/函数,有以下三种方式:
• 指定命名空间访问,项目中推荐这种方式。
• using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
• 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
(只有一个命名空间,或只有少量命名空间且相互之间不冲突时,可以完全展开)
总结。
- 指定域。
- 展开命名空间。(不指定域也不会报错,会到展开的命名空间中去查找)
- 展开某个成员。
- 完全展开。
#include<stdio.h>
#include<stdlib.h>namespace bit
{int rand = 0;int x = 0int y = 0
}
// 指定命名空间访问——最安全
int main()
{printf("%d\n", rand); //默认全局——库函数rand()printf("%d\n", bit::rand);return 0;
}// using将命名空间中某个成员展开——某个频繁使用的成员
using bit::x;
int main()
{printf("%d\n", bit::y);printf("%d\n", x);printf("%d\n", x);printf("%d\n", x);printf("%d\n", x);printf("%d\n", x); //x经常使用,y偶尔使用return 0;
}// 展开命名空间中全部成员——最危险
using namespce bit;
int main()
{printf("%d\n", rand);printf("%d\n", x);printf("%d\n", y);return 0;
}
展开命名空间后不指定域也不会报错的前提——和其他展开的命名空间域 or 全局域不冲突。
故虽然麻烦一点,尽量还是别展开。
展开头文件:把头文件的内容在预处理阶段拷贝过来。
展开命名空间:命名空间是一个域,域的展开是开放访问权限,域的作用是影响编译时查找——先到局部域,没有再到全局域,最后如果有展开的命名空间,就会到展开的命名空间内查找。
编译默认查找
a、当前局部域 : 自留地
b、全局域找 : 村子野地
b、到展开的命名空间中查找 : 相当于张大爷在自己的自留地加了声明,谁需要就来摘
没加这个声明时,默认不会到命名空间中去找。
- 注意展开不是放到全局域,展开后仍然是两个域,只是展开的命名空间域、全局域在查找标识符的出处的时候,具有等价的优先级——局部域之后。
展开的命名空间域、全局域有同名函数不会报错——不调用就不会报错。
一旦调用就会产生调用歧义——调用哪个都可以,全局域、命名空间域都能去找。
(不指定的情况下,全局域、展开的命名空间域都会同等优先级地搜索)
但是指定域的方式去调用也不会出错
::func(); //指定全局域里面去找//或者bit::func() //指定命名空间域里面去找
有了命名空间,就能很好地解决命名冲突的问题。
类型的“域限定符”放在类型名前面——Node前面。(标识符前面)
编译器的一个很智能的点:
应用:
总结——命名空间的价值
命名空间就是把某块空间圈起来, 圈起来之后影响了查找规则,以此解决了命名冲突。
(冲突的本质:不知道用哪一个)
2. C++输入&输出
c++搞了一套新的输入输出流——IO流 / iostream。
新的头文件“iostream” —— 相当于是stdio.h的进化版。
也可以继续包含stdio头文件使用 printf/scanf,但是c++更喜欢使用cout/cin——因为其在命名空间std内,产生隔离更安全。
这里的cout中的c不是c++的c,而是console(控制台)的 c。
windows下的控制台,相当于linux下的终端。
• <iostream> 是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输
出对象。
为什么头文件iosream没有.h——特别老的c++标准带.h(还没有命名空间),如老编译器vc6.0就可以用。
后来出了命名空间,就包到新的不带.h的头文件里了。
头文件.h只是一个标识,现在c++标准库的头文件几乎都不带.h。
C++对C兼容的时候,对C的头文件都封装了一个不带.h的版本。
不带.h的版本,就是用命名空间封装过中的版本。(<stdlib.h> == <cstdlib>)
• std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输
入流。
• std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
• std::endl 是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
• << 是流插入运算符, >> 是流提取运算符。
(C语言还用这两个运算符做位运算左移/右移)
cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出。
他们都包含在包含< iostream >头文件中。
• 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,不需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到).
- 其实最重要的是 C++的流能更好的支持自定义类型对象的输入输出。
• IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识一下C++ IO流的用法,后面我们会有专门的一个章节来细节IO流库。
• cout/cin/endl 等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中。
编译器查找的时候,默认先去局部,再去全局,全局域包含了头文件,头文件展开后按理来说应该有了,但是C++的标准库里面做了一件事——为了防止标准库里面的东西和程序员自己定义的东西冲突了,所以标准库里面的代码被封装进了一个命名空间——std。
直接使用cout,并不会到命名空间std中去查找。
所以要通过命名空间的使用方式去用他们——3种方式。
① 指定命名空间域
② 完全展开
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
int main()
{int a = 0;double b = 0.1;char c = 'x';cout << a << " " << b << " " << c << endl;std::cout << a << " " << b << " " << c << std::endl;scanf("%d%lf", &a, &b);printf("%d %lf\n", a, b);// 可以自动识别变量的类型cin >> a;cin >> b >> c; //可以连续提取——不需要取地址cout << a << endl;cout << b << " " << c << endl; //可以连续插入cout << b << " " << c << '\n'; //换行有两种方式://'\n'//endlreturn 0;
}
③ 指定展开
cin
- 作用:从console里面把输入的数据拿出来放到这个对象(C习惯叫变量,c++习惯叫对象)里面。
- 优点:不用指定格式;也不需要取地址。
- cin不常用就不用指定展开。
• 一般日常练习中我们可以using namespace std,实际项目开发中不建议using namespace std。
• 这里我们没有包含<stdio.h>,也可以使用printf和scanf,在包含<iostream>间接包含了。vs系列 编译器是这样的,其他编译器可能会报错。
cout、cin优点
- 可以“自动识别类型”(printf需要指定类型:%d、%s......),即可以随便插入,不用考虑占位符
- 并且可以连续地输出cout<<i<<j;
- 并且可以在中间自由插入。
- cout<<i<<"abcd"<<j;(插入字符串)
- cout<<i<<" "<<j;(插入空格隔开)
printf、scanf优点
- 更高效(99%的场景都不需要考虑这个)——因为C++要兼容C语言,会有一定的效率的影响
#include<iostream>
using namespace std;
int main()
{// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码// 可以提⾼C++IO效率ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);return 0;
}
• cin、cout也有相关的精度控制函数,默认是有多少输出多少。
c++.ostream
cout需要控制精度、宽度时非常麻烦,涉及一系列函数——>能兼容printf就用printf。
但是VS下的iostream是包含了printf、scanf,Linux下就不一定了,可能会报错,就需要多包一个头文件。
竞赛的2个tips
① C++IO效率
#include<iostream>
using namespace std;
int main()
{// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码// 可以提⾼C++IO效率ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);return 0;
}
② 万能头文件
万能头文件<bits/stdc++.h>:把c++常见的基本都包进来了——但是VS不支持。
但是一展开就会导致程序变大很多,日常、项目都不建议使用,竞赛可用。
展开头文件有极大的销耗。
3. 缺省参数
• 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参。
(有些地方把缺省参数也叫默认参数)
缺省参数分为:全缺省参数、半缺省参数。
• 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。
• C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
(从左往右缺省带有歧义)
从右往左给缺省值,函数调用就不存在歧义——调用函数的实参按形参列表从左往右依次给到形参
• 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
• 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定只能由函数声明给缺省值。——如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
• 缺省值必须是常量或者全局变量。
• C语言不支持(编译器不支持)。
#include <iostream>
#include <assert.h>
using namespace std;void Func(int a = 0)
{ cout << a << endl;
}int main()
{Func(); // 没有传参时,使用参数的默认值Func(10); // 传参时,使用指定的实参return 0;
}
缺省参数的优势:
可以不传参数,函数调用按默认值运行——可传,可不传,提高了程序的灵活度。
#include <iostream>
using namespace std;// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{cout << "a = " << a << endl; cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}// 半缺省——从右往左缺省 && 不能间隔着给
void Func2(int a, int b = 10, int c = 20)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}int main()
{//带缺省参数的函数,函数调用就有多种方式了Func1();Func1(1);Func1(1,2); //规定是按顺序传,传一个参数就是给a,传两个参数就是给a,b——所以不允许间隔着给缺省值 //Func1(1, ,3) //不能跳跃着传Func1(1,2,3); Func2(100);Func2(100, 200);Func2(100, 200, 300);return 0;
}
缺省参数的用途——C的栈的初始化(开0个空间)是实现得不太好的。
在首次插入时开4个空间,后续扩容2倍。
// Stack.h
#include <iostream>
#include <assert.h>
using namespace std;typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;
void STInit(ST* ps, int n = 4);// Stack.cpp
#include"Stack.h"
// 缺省参数不能声明和定义同时给
void STInit(ST* ps, int n)
{assert(ps && n > 0);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;
}// test.cpp
#include"Stack.h"
int main()
{ST s1;STInit(&s1);// 确定知道要插⼊1000个数据,初始化时⼀把开好,避免扩容ST s2;STInit(&s2, 1000);return 0;
}
定义一个栈——>对栈初始化——>插入1000个数据 这个时候会导致一个问题——>这个程序会有大量的扩容——>而且扩容到后面,异地扩容消耗很大(如果没有足够的空间:开新的空间——拷贝数据——释放旧的空间),而且越往后消耗越大(拷贝空间、拷贝时间)
C语言传统的解决方案:
(1)定义一个宏N,一开始就先开一堆空间。
这样写死的方式都是不太好的,因为当初始数据较小时就会造成比较大的空间浪费。
(2)增加一个参数n,灵活一点,能够帮助我们去控制,即要初始化多少你直接给我
(最好不要使用宏——>不好用)
(3)在C++,如果一开始不知道要初始化多大的空间,就可以给参数一个官方指导值(缺省值)
有了缺省参数,就能在初始化时不去指定具体开辟多大的空间,不指定就默认申请4字节的空间。
——体现半缺省的价值:知道要开多大就传多大,不知道就开默认值。
4. 函数重载
4.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这 些同名函数的形参列表(参数个数、类型、类型顺序)不同,常用来处理实现功能类似、数据类型不同的问题。
函数重载的要点
- 同一作用域:同名函数——>形参不同(个数不同、类型不同)。
这样C++函数调用就表现出了多态行为,使用更灵活。
C语言是不支持同一作用域中出现同名函数的。
error C2084:“函数“int Add(int,int)”已有主体
#include<iostream>
using namespace std;// 1、参数类型不同
int Add(int left, int right)
{cout << "int Add(int left, int right)" << endl;return left + right;
}double Add(double left, double right)
{cout << "double Add(double left, double right)" << endl;return left + right;
}// 2、参数个数不同
void f()
{cout << "f()" << endl;
}void f(int a)
{cout << "f(int a)" << endl;
}// 3、参数类型顺序不同
void f(int a, char b)
{cout << "f(int a,char b)" << endl;
}void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{Add(10, 20);Add(10.1, 20.2);f();f(10);f(10, 'a');f('a', 10);return 0;
}
c++可根据形参匹配同名函数
(同一作用域内的同名函数——都在同一命名空间、或都在全局)
(不同域本就允许同名函数)。
之前是在不同作用域,即使有“不去访问未展开的命名空间”的规定,也可以根据参数去调用命名空间内的函数。
原理都是参数匹配。
4.1.1 不构成函数重载的情况
(1)返回值不同
void fxx()
{//……
}int fxx()
{return 0;
}
返回值不同是不构成重载的,函数重载根本就不看返回值,有返回值没有返回值都没关系,只看参数列表。
因为看返回值的不同,无法区分函数调用,调用时会根据参数去匹配函数。
换一个角度看,返回值在调用的时候并不是必须的,返回值可以不接收,但是参数必须传递。
上述代码结果:报错。
(2)缺省值不同
void f(int a = 10)
{//……
}void f(int a = 20)
{//……
}
上述代码结果:报错。
(3)不同的(命名空间)域
//情景1:两个同名函数——不构成重载,但是可以同时存在
namespace bit1
{void func(int a){//……}
}namespace bit2
{void func(int b){//……}
}
情景2:若是都叫bit1——不构成重载,不能同时存在。
还是在同一命名空间(会合并),那同一作用域函数要同名存在,必须满足重载规则。
4.1.2 函数重载的调用歧义
// 下⾯两个函数构成重载
// f()但是调用时,会报错,存在歧义,编译器不知道调用谁
void f1()
{cout << "f()" << endl;
}void f1(int a = 10)
{cout << "f(int a)" << endl;
}int main()
{f1();return 0;
}
不调用就不会报错
全缺省和无参的同名函数,构成函数重载,但是调用f()会存在歧义。
调用全缺省的函数时给参数,也不会发生调用歧义。
4.1.3 非函数重载的调用歧义
调用歧义:两个swag(int*,int*)都可以调——swag(&a,&b)不知道调哪一个 。
但是swag(&c,&d)知道——只有全局域有其定义swag(double*,double*)
展开命名空间,两个swap还是在各自的域中,不构成函数重载。
(不构成函数重载但是都可以存在,不调用就不会出错,这里的错误在于调用歧义)
命名空间都展开——不构成重载关系——还是在各自的作用域,同一个域内才有重载的概念。
重载:同一个菜地。
展开:菜地旁插了块牌子。
4.1.4 隐式类型转换
//只有一个函数的时候——才存在隐式类型转换
void f(int a, char b)
{cout << "f(int a,char b)" << endl;
}//void f(char b, int a)
//{
// cout << "f(char b, int a)" << endl;
//}int main()
{int a = 0, b = 1;double c = 0.1, d = 1.1;f(1, 'a');f('a', 1); //只有一个f(int,char),调用f(char,int)会有转换return 0;
}//存在多个函数同名:只有匹配的问题,没有转换的问题——不让转
void f(int a, char b)
{cout << "f(int a,char b)" << endl;
}void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{int a = 0, b = 1;double c = 0.1, d = 1.1;f(1, 'a');f('a', 1);//两个同名函数,这个调用就存在歧义f('a', 'a')//这个调用是带有条件的,条件就是隐式类型转换//调用第一个f(int,char)就是一参char——>int//调用第二个f(char,int)就是二参char——>int//这里的调用就存在歧义,不知道调哪一个——调用必须得没有歧义return 0;
}
4.2 函数重载的原理
C++支持函数重载的原理--名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
——函数名修饰规则
注意点:只要函数调用去匹配函数声明的时候,在参数上是匹配的,那么这个调用就是合法的,即函数调用的合法性与函数定义无关,只与函数声明有关。
声明和定义分离时,就会有缺地址的问题——地址在外部符号表。
只有声明,没有定义(没有地址),call指令就只检查:
使用符不符合规则—参数匹不匹配—语法正不正确
语法不正确:报出语法错误。
语法正确:在链接的时候拿函数名去符号表里面找地址。
直接拿函数名去找吗???
前面讲这么多就是为了说明:
- 在链接时有这么一个查找的过程,为什么C不支持,而c++支持?
- 因为在链接的时候要用函数名去找地址,如果声明和定义分离,即包.h一起编译的汇编代码缺地址,而调用一个函数,要用函数名去找,C语言直接用函数名去找,就区分不开。(声明和定义没有分离,是不找的)
- 而c++用修饰后的函数名去找,不同的编译器有具体的函数名修饰规则(把参数带进来),但都会把函数的参数的类型带进来。
- 大概就是公共前缀+函数名+形参类型。
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
1. 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么怎么办呢?
2. 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。
……
3. 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。
4. 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字。
5. 通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
采用C语言编译器编译后结果
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
采用C++编译器编译后结果
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
Windows下名字修饰规则
对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的,我们就不做细致的研究了。
【扩展学习:C/C++函数调用约定和名字修饰规则--有兴趣好奇的同学可以看看,里面有对vs下函数名修饰规则讲解】
C/C++的调用约定
6. 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
7. 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
法区分。