菜鸟笔记
提升您的技术认知

重载原理及Linux查看符号表

0.定义

函数重载:在相同作用域中的多个函数,具有相同的名字而形参表不同。

不能仅仅基于不同的返回类型而实现函数重载。返回值不影响函数签名的。

1.原理

C++函数重载底层实现原理是C++利用name mangling(倾轧)技术,来改名函数名,区分参数不同的同名函数

编译器通过函数名和其参数类型 识别重载函数。为了保证类型安全的连接(type-safe linkage),编译器用参数个数参数类型对每一个函数标识符进行专门编码,这个过程有时称为“名字改编”(name mangling)或“名字修饰”(name decoration)。类型安全的连接使得程序能够调用合适的重载函数并保证了参数传递的一致性。编译器能够检测到并报告连接错误。

2.查看符号表

  1. nm 目标文件
  2. objdump -t 目标文件
  3. readelf -s 目标文件
  4. strings 目标文件

2.1 gcc编译

  • test.c
int func(int a){
  };
  • 编译
gcc -c test.c -o test.o
  • 查看符号
objdump -t test.o
函数名 名字改编
int func(int a); func

2.2 g++编译

  • test.cpp
int func(int a){
  };
int func(int a,int b){
  };
  • 编译
g++ -c test.cpp -o test.o
  • 查看符号
objdump -t test.o
函数名 名字改编
int func(int a); _Z4funci
int func(int a,int b); _Z4funcii

_Z规定前缀4是函数名的字符个数i参数列表类型i首字母

注意:不同系统和编译器的命名倾轧方式是有所不同的。

3.命名反倾轧

  1. 名字改编转化成函数名
    使用c++filt命令可以很容易把名字改编转换成函数名。
    例如:
c++filt _Z4funci
  1. 查看反倾轧的符号表
nm -C 目标文件

4.禁用命名倾轧

C++命名倾轧的函数是无法被C语言调用的。C++的函数必须是没有倾轧的才能调用。
使用声明extern "C"的函数会禁止命名倾轧,这样C++的函数就可以被C语言调用。
例如:

  • sum.h
#ifndef __CPP_H_
#define __CPP_H_

#ifdef __cplusplus
extern "C" {
  
#endif // __cplusplus

int sum(int* arr,int n);

#ifdef __cplusplus
}
#endif // __cplusplus

#endif // __CPP_H_

通常为了头文件可以同时被C++/C使用,通常把extern "C"放在__cplusplus条件宏定义中。

  • sum.cpp
#include "cppFunc.h"
#include <numeric>
using namespace std;

int sum(int* arr,int n){
  
    return accumulate(arr,arr+n,0);
}
  • sum_test.c
#include <stdio.h>
#include "cppFunc.h"
int main(){
  
    int arr[] = {
  1,2,3,4,5};
    printf("%d\n",sum(arr,5));
}

编译

g++ -c sum.cpp
gcc sum_test.c sum.o

4.1实践

vector<int>封装成C语言可调用的顺序表。

gcc调用找不到C++标准库符号时,-lstdc++链接C++标准库

  • 头文件部分内容
#ifdef __cplusplus
    extern "C"{
  
#endif // __cplusplus

void* seq_create();
void seq_destroy(void* seq);
void seq_append(void* seq,int val);
void seq_prepend(void* seq,int val);
int seq_size(void* seq);
int seq_get(void* seq,int index);

#ifdef __cplusplus
}
#endif // __cplusplus
  • 源文件部分内容
    i
nt max_element(int* arr,int n){
  
    return max_element(arr,arr+n) - arr;
}
void* seq_create(){
  
    return new vector<int>();
}
void seq_destroy(void* seq){
  
    delete reinterpret_cast<vector<int>*>(seq);
}
void seq_append(void* seq,int val){
  
    reinterpret_cast<vector<int>*>(seq)->push_back(val);
}
void seq_prepend(void* seq,int val){
  
    vector<int>* p = reinterpret_cast<vector<int>*>(seq);
    p->insert(p->begin(),val);
}
int seq_size(void* seq){
  
    return reinterpret_cast<vector<int>*>(seq)->size();
}
int seq_get(void* seq,int index){
  
    return reinterpret_cast<vector<int>*>(seq)->at(index);
}

5.原理小结



extern "C"放在__cplusplus条件宏定义中,头文件可以同时被C++/C使用!

6.空类

6.1 空类的大小是多少?

一个类是没有大小的,应该说是类的实例的大小

class A;
A a;

其中,sizeof(A) == sizeof(a);
一个对象的大小 大于等于所有的非静态成员大小的总和

空类进行实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器一般会给一个空类隐含的加一个字节.

所以,空类的大小是1.

6.2 编译器会给空类自动生成几个成员函数?

  1. 默认(缺省的)构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符重载函数
  5. 两个取址运算符重载函数
class Empty {
  
  public:
    Empty(){
  }                            //缺省构造函数
    Empty(const Empty &rhs){
  }            //拷贝构造函数
    ~Empty(){
  }                           //析构函数 
    Empty& operator=(const Empty &rhs){
  } //赋值运算符重载函数
    Empty* operator&(){
  }                 //取址运算符重载函数
    const Empty* operator&() const{
  }     //取址运算符重载函数(const版本)
};

调用时机

Empty *e = new Empty();    //缺省构造函数
delete e;                  //析构函数
Empty e1;                  //缺省构造函数                               
Empty e2(e1);              //拷贝构造函数
Empty e3 = e1;             //拷贝构造函数
e2 = e1;                   //赋值运算符
Empty *pe1 = &e1;          //取址运算符重载函数(非const)
const Empty *pe2 = &e2;    //取址运算符重载函数(const)

一个类中成员函数、虚函数、静态数据成员都是不占用类的存储空间的。