0000000000000400623<_Z3addii>
000000000000040062f<_Z3adddd>
test.o
// add(1,2);
call \_Z3addii(?)
// add(1.1,2.2);
call \_Z3adddd(?)
- C++的函数修饰规则(不同编译器不同规则),但都把参数类型加进去了
_Z+函数长度+函数名+类型首字母
因此函数命名不同了之后链接的时候去其他目标文件中找符号表就能找到对应的函数。
//add(1,2)
call \_Z3addii(?) //link的时候去找?的地址
//其他目标文件中\_Z3addii符号的地址是000000004007dd
//add(1.1,2.2)
call \_Z3adddd(?) //link的时候去找?的地址
//其他目标文件中\_Z3adddd符号的地址是000000004007ff
C语言不支持函数重载,因为编译的时候,两个重载函数,函数名相同,在同一个目标文件.o中,符号表中存在歧义和冲突。同样的函数名两个函数地址。其次链接的时候也存在歧义和冲突,因为他们都是直接使用函数名去标识和查找。而重载函数,函数名相同。
而C++的目标文件符号表中不是直接用函数名来标识和查找函数
- 函数名修饰规则(不同编译器下函数修饰名不同)
- _Z+函数长度+函数名+类型首字母
- 有了函数名修饰规则,只要参数不同,目标文件的符号表里面就不存在二义性和冲突了
- 链接的时候,test.o的main函数里面去调用两个重载的函数也是明确的。
5.3extern “C”
vs下实现静态库:
- 包含对应目录下的头文件
- 在工程属性中配置静态库目录(链接器的常规中),添加静态库(链接器的输入中)
Cpp调C:在Cpp的#include"…/xxx/xx.h"上下加上extern “C”{}
extern "C"
{
#include"../xx/Stack.h"
}
C调Cpp:在Cpp的.h文件中加extern “C”{ 函数},Cpp静态库就会按照C的规则去处理以下函数。当然重载就要写两个函数了。
特别注意,因为.c包含了.h的头文件,所以头文件包含会将.h部分在c展开,而cpp才认识extern “C”,C部分不认识extern “C”;
- 第一种做法
#ifdef \_\_cplusplus
extern "C" {
#endif
cpp函数声明;
#ifdef \_\_cplusplus
}
#endif
引入条件编译,在cpp库中,是识别extern "C"并且按照C的方式进行函数推导。当C对其头文件展开的时候,由于不是cpp,条件编译直接声明成C函数。
- 第二种做法
#ifdef \_\_cplusplus
#define EXTERN\_C extern "C"
#else
#define EXTERN\_C
#endif
EXTERN_C cpp函数声明;
EXTERN_C cpp函数声明;
EXTERN_C cpp函数声明;
**也就是说extern “C” 总是在cpp中的,因为只有cpp 认识 extern " C" **
C++程序调用C的库,在C++程序中加extern “C”
C程序调用C++的库,在C++库中加extern “C”
参考文章:https://zhuanlan.zhihu.com/p/361485807
5.3.1C++程序中调用C库
首先为什么C++程序中不能调用C库,会产生链接错误。因为两者对函数名字的命名规则不同,因此C++的链接器会去C模块中查找对应函数,但是找不到。
那我们怎么在C++项目中使用C库模块的?
//util.h
extern "C"
{
int add(int ,int );
}
通过extern “C”,告诉g++编译器,不要对这些函数进行Name mangling,按照C编译器的方式去生成符号表符号。这样在main.c的目标文件(.o)中,参数列表为两个int类型的add函数名称为_add。链接器可以正确找到util.o中的add函数(他们都是_add)。
不过注意参数列表为两个double类型的add函数名称还是__Z3adddd。
使用 extern ”C“ 的常见情况是使用第三方提供的编译好的静态链接库(.a/.lib),动态链接库(.so/.dll)。通常我们会拿到一个头文件和对应的编译好的库文件。
在头文件中通过条件编译引入 extern “C”。
//until.h
#ifdef \_\_cplusplus
extern "C" {
#endif
int add(int, int);
#ifdef \_\_cplusplus
}
#endif
gcc -c xxx.c
ar -rc libxxxx.a xxx.o xxx.o
test-static:test.cc
g++ -o $@ $^ -I ./mylib/include -L ./mylib/lib -l util -static
.PHONY:clean
clean:
rm -rf *.o test-static
5.3.2C程序中调用C++函数
假设我们有一个C++类 Robot,在文件 robot.h 和 robot.cpp 中定义。Robot 类中有个成员函数 sayHi() 我们想在C程序中调用这个函数。
robot.h
#pragma once
#include <string>
class Robot
{
public:
Robot(std::string name) : name\_(name) {}
void sayHi();
private:
std::string name_;
};
robot.cpp
#include <iostream>
#include "robot.h"
void Robot::sayHi()
{
std::cout << "Hi, I am " << name_ << "!\n";
}
我们用编译C++代码的方式,使用 g++ 编译器对这个类进行编译,此时类 Robot 并不知道自己会被C程序调用。
g++ -fpic -shared robot.cpp -o librobot.so
接下来用C++创建一个C的接口,定义在 robot_c_api.h 和 robot_c_api.cpp 中,这个接口会定义一个C函数 Robot_sayHi(const char *name), 这个函数会创建一个类 Robot 的实例,并调用 Robot 的成员函数 sayHi()。
robot_c_api.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void Robot_sayHi(const char *name);
#ifdef __cplusplus
}
#endif
robot_c_api.cpp
#include "robot_c_api.h"
#include "robot.h"
#ifdef __cplusplus
extern "C" {
#endif
// 因为我们将使用C++的编译方式,用g++编译器来编译 robot_c_api.cpp 这个文件,
// 所以在这个文件中我们可以用C++代码去定义函数 void Robot_sayHi(const char *name)(在函数中使用C++的类 Robot),
// 最后我们用 extern "C" 来告诉g++编译器,不要对 Robot_sayHi(const char *name) 函数进行name mangling
// 这样最终生成的动态链接库中,函数 Robot_sayHi(const char *name) 将生成 C 编译器的符号表示。
void Robot_sayHi(const char *name)
{
Robot robot(name);
robot.sayHi();
}
#ifdef __cplusplus
}
#endif
同样用编译C++代码的方式进行编译
g++ -fpic -shared robot_c_api.cpp -L. -lrobot -o librobot_c_api.so
现在我们有了一个动态链接库 librobot_c_api.so, 这个动态链接库提供了一个C函数 Robot_sayHi(const char *name),我们可以在C程序中调用它了。
main.c
#include "robot_c_api.h"
int main()
{
Robot_sayHi("Alice");
Robot_sayHi("Bob");
return 0;
}
使用C程序的编译方式,用 gcc 对 main.c 进行编译
gcc main.c -L. -lrobot_c_api
可以看到 gcc 编译出的函数符号和 librobot_capi.so中 g++ 编译器编译出的函数符号一致。这样最终在我们的C程序中可以正确的链接到动态库中的Robot_sayHi(const char *name) 函数。
注意这个.c文件一定要用g++
编译,不然识别不了c++的头文件,导致折腾了很久。
test:test.c
g++ -o $@ $^ -I ./mylib -L ./mylib -l robot_c_api
.PHONY:clean
clean:
rm -rf test
librobot_c_api.so:robot.o robot_c_api.o
g++ -shared -o $@ $^
robot_c_api.o:robot_c_api.cc
g++ -fPIC -c $<
robot.o:robot.cc
g++ -fPIC -c $<
.PHONY:clean
clean:
rm -rf *.o mylibrobot librobot_c_api.so librobot.so output
.PHONY:output
output:
mkdir -p ./output/include
mkdir -p ./output/lib
cp ./*.so ./output/lib
cp ./*.h ./output/include
export LD\_LIBRARY\_PATH=/home/ycb/demo1/back-Cpp-C-extern/Cpp-C-extern/Cpp-C-extern/mylib
5.4相关问题
- 下面两个函数能形成函数重载吗?有问题吗或者什么情况下会出问题?
void f(){
cout<<"f()"<<endl;
}
void f(int a=0){
cout<<"f(int a)"<<endl;
}
int main(){
f();//error:调用存在歧义
}
- C语言中为什么不能支持函数重载?
- C++中函数重载底层是怎么处理的?
- C++中能否将一个函数按照C的风格来编译?
6.引用
6.1引用概念
#include<iostream>
using namespace std;
int main()
{
int a=1;
int& ra=a; //ra是a的引用,引用也就是别名。a再取了一个名称ra
int& raa=ra;
}
引用在物理空间上的意义:
引用就是给一个变量再取新的名字。编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间
类型名& 引用变量名(对象名)=引用实体
6.2引用特性
-
引用必须在定义时初始化
int a=1;
int& b;///error -
-
一个变量可以有多个引用
-
引用一旦引用一个实体,不能发生变化
//1.引用必须在定义时必须初始化
int main(){
int a=10;
int &b;
}
//2.一个变量可以有多个引用
int main(){
int a =10;
int&b =a;
int&c =a;
int&d =b;
}
//3.引用一旦引用一个实体,再不能引用其他实体
int main(){
int a=10;
int &b=a;
int c=20;
b=c;///分析:这里是c变成了d的引用?还是d赋值给c(yes)
}
6.3常引用
总结:
- 引用取别名时,变量访问权限可以缩小,不能放大。
- 权限的放大和缩小规则:适用于引用和指针。不适用变量之间的赋值。
6.3.1权限的缩小与放大
int main()
{
int a=0;
int& b=a; //b的类型是int
const int a=0;
int&b = a;//error:编译不通过。原因:a是const,但是不能修改,b的类型是int,也就是可读可写,那么逻辑上就会产生矛盾。
const int& b=a;//right
int c=1;
int& d=c;
const int& e =c;行不行?可以->c是可读可写的,e变成别名是只读,逻辑上是可以的。
//变量之间赋值没有权限缩小和放大的关系,引用才有
const int ci =i ;
int x=ci;
return 0;
}
int main()
{
const int\* cp1=&a;
int\* p1=cp1;//error: const int\* cp1表示cp1指向的内容不能更改。而int\* p1=cp1如此赋值表明p1可以修改该块内存。逻辑错误。权限的放大。
int\* p2=&c;
const int\* cp2=p2; //权限缩小,ok
}
void f(int &x){
cout<<x<<endl;
}
int main(){
const int a=10;
const int &b=a;
f(a);//error:权限的放大。
f(b);
}
void f(const int& x){
cout<<x<<endl;
}
int main(){
const int a=10;
const int& b=a;
f(a);
f(b);
}
特别地,当涉及到类与对象的时候,对于this
指针的问题。
类中的函数的隐藏this
指针修饰对象和普通函数const参数对象直接调用的权限不对等
class A{
public:
double get\_avg\_score(){};
}
void fun(const A& a){
a.get\_avg\_score();/\*会报错\*/
}
void fun(const A& a) const{
a.get\_avg\_score();
}
在类中的函数后面加const
对this
指针进行修饰即可。
权限的放大和缩小规则:适用于引用和指针。不适用变量之间的赋值。
不用引用的话传参无所谓,只是对象之间的赋值。const对象拷贝给x。
void f(int x){
cout<<x<<endl;
}
int main(){
const int a=10;
const int &b =a;
f(a);
f(b);
}
6.3.2产生右值的场景
类型转化,类型截断,类型提升函数返回值,函数传参,都会产生临时变量。
int main()
{
int i=0;
double db=i; //隐式类型转换
double& rdb=i;//error
float& rf=i;//error:和字节大小无关
//但是+const就可以
const double& rd=i;
const float& rf=i;
}
隐式类型转换的赋值是怎么产生的?
6.4引用场景
1、引用做参数
- 输出型参数
- 提高效率
回顾之前单链表的PushBack部分,我们要注意传递二级指针来处理原来指针变量的值。
void SLiPushBack(STLNode\*\* pphead,SLTDataType x){
assert(pphead);
STLNode\* newnode=CreateSListNoded(x);
if(\*pphead==NULL){
\*pphead=newnode;
return;
}
else{
STLNode\* tail=\*pphead;
while(tail->next!=NULL) tail=tail->next;
tail->next=newnode;
}
}
int main(){
SLTNode\* plist=NULL;
SListPushBacn(&plist,1);
SListPushBacn(&plist,2);
SListPushBacn(&plist,3);
SListPushBacn(&plist,4);
SListPushBack(plist);
return 0;
}
有了引用之后,就可以省去一层二级指针。
int main(){
int a=10;
int& b=a;
int \*p1=&a;
int \*&p2=p1;
}
void SLiPushBack(STLNode\*& pphead,SLTDataType x){
assert(pphead);
STLNode\* newnode=CreateSListNoded(x);
if(pphead==NULL){
pphead=newnode;
return;
}
else{
STLNode\* tail=pphead;
while(tail->next!=NULL) tail=tail->next;
tail->next=newnode;
}
}
int main(){
SLTNode\* plist=NULL;
SListPushBacn(&plist,1);
SListPushBacn(&plist,2);
SListPushBacn(&plist,3);
SListPushBacn(&plist,4);
SListPushBack(plist);
return 0;
}
再比如做C语言的题的时候给定接口的int* returnSize
就是一个输出型参数。
void swap\_c(int\* p1,int \*p2)
{
int tmp=\*p1;
\*p1=\*p2;
\*p2=tmp;
}
void swap\_cpp(int &r1,int &r2)
{
int tmp=r1;
r1=r2;
r2=tmp;
}
int main()
{
int a=0;int b=1;
swap(&a,&b);
swap\_cpp(a,b);
return 0;
}
前面说到引用定义的时候要初始化,这里引用定义的地方在传参。
2、引用做返回值
总结:
- 凡是传值,不管是参数还是址,都会产生拷贝变量。传引用不会。
- 一个函数要使用引用返回,返回变量出了这个函数的作用域还存在,就可以使用引用返回,否则不安全。
- 全局变量、静态变量等
- 函数使用引用返回的好处是什么
- 少创建拷贝一个临时对象,提高效率。
- 其实还有一个作用,以后再补充。
- 修改返回对象如operator[] (已补充–模板初阶模板类)
先来回顾一下传值返回。
所有的传值都会生成一个拷贝
int Add(int a,int b)
{
int c=a+b;
return c;
}
int main(){
int ret=Add(1,2);
cout<<ret<<endl;
return 0;
}
我们可以看到调用Add(int,int)
函数的过程return c
的过程中,将计算出来的c变量
的值存到了临时变量%eax
寄存器中,然后再传给main
函数中的ret
变量。
临时变量存在哪里呢?
- 如果
c
如果比较小(4 or 8),一般是寄存器充当临时变量。 - 如果
c
比较大,临时变量放在调用Add函数的栈帧中。
而传引用返回就是不会生成c的拷贝返回,直接返回c的引用
int Add1(int a,int b)
{
int c=a+b;
return c;
}
int main()
{
const int& ret=Add1(1,2);//临时变量具有常性
Add1(3,4);
cout<<"Add1(1,2) is:"<<ret<<endl;
}
int& Add2(int a,int b)
{
int c=a+b;
return c;
}
int& Add2(int a,int b)
{
static int c=a+b;
return c;
}
int main()
{
int& ret=Add2(1,2);//ret就是c的别名。(实际上是c这块空间的别名).
//销毁不意味着清除,是没有使用权。
Add2(3,4);
cout<<"Add2(1,2) is:"<<ret<<endl;///ret输出为7了。引用返回是不安全的。
//说明如果返回变量c是一个局部变量时,引用返回是不安全的。
return 0;
}
出了作用域还是返回已经销毁的栈帧(未有使用权)。不能保证原来的结果,其他函数能改这一块的。就会产生问题。
如何解决这个问题?
加static。
void test()
{
static int a=1;///第二次不执行
a++;
printf("%d",a);
}
此时c不在Add2的栈帧。
所以第二次Add(3,4)的时候static int c=a+b是不执行的。这份代码是只能是3。不过只有本函数才能改自己的c。
int Count1()//传值返回
{
static int n=0;
n++;
return n;//返回临时变量
}
int& Count2()//传引用返回
{
static int n=0;
n++;
return n;//没有额外空间
}
int main()
{
int& r1=Count1();//error:r1想成为临时变量的别名,因为临时变量具有常性。所以不行。需要加const
int& r2=Count2();//tmp相当于n的别名。r2相当于tmp的别名。
return 0;
}
6.5传值,传引用的效率比较
#include<ctime>
struct A{
int a[10000];
};
A a;
A TestFunc1() {return a;}
A& TestFunc2() { return a;}
void main()
{
size_t begin1=clock();
for(size_t i=0;i<10000;i++)
{
TestFunc1();
}
size_t end1=clock();
cout<<end1-begin1<<endl;
size_t begin2=clock();
for(size_t i=0;i<10000;i++)
{
TestFunc2();
}
size_t end2=clock();
cout<<end2-begin2<<endl;
}
#include<ctime>
struct A{
int a[10000];
};
void TestFunc1(A a) {return a;}
void TestFunc2(A& a) { return a;}
void main()
{
A a;
//以值作为函数参数
size_t begin1=clock();
for(size_t i=0;i<10000;i++)
{
TestFunc1(a);
}
size_t end1=clock();
//以引用作为函数参数
size_t begin2=clock();
for(size_t i=0;i<10000;i++)
{
TestFunc2(a);
}
size_t end2=clock();
}
总结一下:引用的作用主要体现在传参和传返回值。
- 引用传参和传返回值,有些场景下面,可以提高性能。(大对象+深拷贝对象)
- 引用传参和传返回值,输出型参数和输出型返回值。通俗点说,有些场景下面,形参的改变可以改变实参。
有些场景下面,引用返回,可以改变返回对象。
6.6引用和指针的区别
在语法层面:指针和引用是完全不同的概念
- 指针是开空间,存储变量地址
- 引用是不开空间,仅仅对变量取别名,没有独立空间,和其引用实体共享一个空间。
- 因此用的时候不要想底层汇编如何实现,只考虑语法层
在底层实现(看反汇编)上,是和指针一样的。
引用和指针的不同点
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但是有NULL指针。
- 在sizeof中含义不同:引用的结果是引用类型的大小,指针的结果是地址的大小。
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式应用,引用编译器自己处理
- 引用比指针使用起来相对更安全
#include<iostream>
using namespace std;
int main()
{
int a=10;
int& b=a;
int\* p =&a;
return 0;
}
00000000000007aa <main>:
7aa: 55 push %rbp
7ab: 48 89 e5 mov %rsp,%rbp
7ae: 48 83 ec 20 sub $0x20,%rsp
7b2: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
7b9: 00 00
7bb: 48 89 45 f8 mov %rax,-0x8(%rbp)
7bf: 31 c0 xor %eax,%eax
7c1: c7 45 e4 0a 00 00 00 movl $0xa,-0x1c(%rbp)
7c8: 48 8d 45 e4 lea -0x1c(%rbp),%rax
7cc: 48 89 45 e8 mov %rax,-0x18(%rbp)
7d0: 48 8d 45 e4 lea -0x1c(%rbp),%rax
7d4: 48 89 45 f0 mov %rax,-0x10(%rbp)
7d8: b8 00 00 00 00 mov $0x0,%eax
7dd: 48 8b 55 f8 mov -0x8(%rbp),%rdx
7e1: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
7e8: 00 00
7ea: 74 05 je 7f1 <main+0x47>
7ec: e8 7f fe ff ff callq 670 <__stack_chk_fail@plt>
7f1: c9 leaveq
7f2: c3 retq
7.内联函数
VS的c/c++中的优化,内联函数扩展,调成只适用于__inline,在debug下就能展开。
7.1内联函数的概念
内联函数的本质是消除函数调用。
一般情况下在Debug下不能展开。但是Release看不到。
需要VS设置。通过反汇编就可以看到没有Call了。
int Add(int left,int right)
{
return left+right;
}
void Swap(int &x1,int &x2)
{
int tmp=x1;
x1 =x2;
x2 =tmp;
}
///频繁调用Swap是有栈帧消耗的
//C语言如何解决:1.C语言使用宏函数(提前展开了)2.C++使用内联函数(会在调用的地方展开)
int main()
{
int ret=Add(1,2);
}
7.2内联函数的特性
- inline函数是一种空间换时间的做法,省去调用函数额外开销。
- Call Swap 假设程序中调用了1w次。假设swap10行指令。此时是10010
- inline之后就没有调用的call。但是展开后指令个数是100000了。
- 一般内联适用于小函数,小于20行。其次递归,长的代码不适用于内联。
- inline对于编译器而言只是一个建议,编译器会自动优化。
- 内联不建议声明和定义分离,分离会导致连接错误。因为inline被展开,就没有函数地址了,链接就找不到。
7.3相关题目
- 宏的优缺点
- 优点:
- 增强代码的复用性
- 提高性能
- 缺点
- 不方便调试宏(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用
- 没有类型安全的检查
- 优点:
- C++的替代
- 用const替换常量定义
- 短小函数定义换用内联函数
8.auto关键字(C++11)
8.1auto的历史
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。auto修饰的变量表示该变量在函数栈帧结束时释放。但是这是自动的。因此C++11给修改了。
8.2typedid(x).name()查看变量的类型
int main()
{
int a=10;
auto b=a;//b的类型是根据a的类型推导出是int
auto c=a;
auto d='A';



**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
加入社区》https://bbs.youkuaiyun.com/forums/4304bb5a486d4c3ab8389e65ecb71ac0
会在调用的地方展开)
int main()
{
int ret=Add(1,2);
}
7.2内联函数的特性
- inline函数是一种空间换时间的做法,省去调用函数额外开销。
- Call Swap 假设程序中调用了1w次。假设swap10行指令。此时是10010
- inline之后就没有调用的call。但是展开后指令个数是100000了。
- 一般内联适用于小函数,小于20行。其次递归,长的代码不适用于内联。
- inline对于编译器而言只是一个建议,编译器会自动优化。
- 内联不建议声明和定义分离,分离会导致连接错误。因为inline被展开,就没有函数地址了,链接就找不到。
7.3相关题目
- 宏的优缺点
- 优点:
- 增强代码的复用性
- 提高性能
- 缺点
- 不方便调试宏(因为预编译阶段进行了替换)
- 导致代码可读性差,可维护性差,容易误用
- 没有类型安全的检查
- 优点:
- C++的替代
- 用const替换常量定义
- 短小函数定义换用内联函数
8.auto关键字(C++11)
8.1auto的历史
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。auto修饰的变量表示该变量在函数栈帧结束时释放。但是这是自动的。因此C++11给修改了。
8.2typedid(x).name()查看变量的类型
int main()
{
int a=10;
auto b=a;//b的类型是根据a的类型推导出是int
auto c=a;
auto d='A';
[外链图片转存中...(img-pYzZpGZ5-1725639450327)]
[外链图片转存中...(img-8WRxtPPU-1725639450327)]
[外链图片转存中...(img-37Ac8yxi-1725639450328)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
加入社区》https://bbs.youkuaiyun.com/forums/4304bb5a486d4c3ab8389e65ecb71ac0