C++知识点(4)

GCC常用参数

-ansi:关闭 gnu c 中与 ansi c 不兼容的特性 , 激活 ansi c 的专有特性 ( 包括禁止一些 asm inline typeof 关键字 , 以及 UNIX,vax 等预处理宏

-lxx:表示动态加载libxx.so库

-Lxx:表示增加目录xx,让编译器可以在xx下寻找库文件

-Ixx:表示增加目录xx,让编译器可以在xx下寻找头文件

-shared:生成共享目标文件。通常用在建立共享库时

-Wall:生成所有警告信息。一下是具体的选项,可以单独使用

简单的GCC语法:

第一是 “-o”,它后面的参数表示要输出的目标文件,再一个是 “-c”,表示仅编译(Compile),不连接(Make),如果没有”-c”参数,那么就表示连接,如下面的几个命令:

gcc –c test.c,表示只编译test.c文件,成功时输出目标文件test.o

gcc –c test.c –o test.o ,与上一条命令完全相同

gcc –o test test.o,将test.o连接成可执行的二进制文件test

gcc –o test test.c,将test.c编译并连接成可执行的二进制文件test

gcc test.c –o test,与上一条命令相同

gcc –c test1.c,只编译test1.c,成功时输出目标文件test1.o

gcc –c test2.c,只编译test2.c,成功时输出目标文件test2.o

gcc –o test test1.o test2.o,将test1.o和test2.o连接为可执行的二进制文件test

gcc –c test test1.c test2.c,将test1.o和test2.o编译并连接为可执行的二进制文件test

注:如果你想编译cpp文件,那么请用g++,否则会有类似如下莫名其妙的错误:

cc3r3i2U.o(.eh_frame+0x12): undefined reference to `__gxx_personality_v0’……

还有一个参数是”-l”参数,与之紧紧相连的是表示连接时所要的链接库,比如多线程,如果你使用了pthread_create函数,那么你就应该在编译语句的最后加上”-lpthread”,”-l”表示连接,”pthread”表示要连接的库,注意他们在这里要连在一起写,还有比如你使用了光标库curses,那么呢就应该在后面加上”-lcurses”,比如下面的写法:

gcc –o test test1.o test2.o –lpthread –lcurses

例如: 在ubuntu 环境下编译基于course库函数的程序时,如果不带 -lncurses时,会出现

screen1.c:(.text+0x12):对‘initscr’未定义的引用
screen1.c:(.text+0x24):对‘wmove’未定义的引用
screen1.c:(.text+0x39):对‘printw’未定义的引用
screen1.c:(.text+0x4a):对‘wrefresh’未定义的引用
screen1.c:(.text+0x5f):对‘endwin’未定义的引用
需使用 gcc -o screen1 screen1.c -lncurses

-x language filename

设定文件所使用的语言,使后缀名无效,对以后的多个有效.
例子用法:
  gcc -x c hello.pig

-x none filename

关掉上一个选项,也就是让gcc根据文件名后缀,自动识别文件类型
例子用法:
  gcc -x c hello.pig -x none hello2.c

-c

只激活预处理,编译,和汇编,也就是他只把程序做成obj文件
例子用法:
  gcc -c hello.c
它将生成.o的obj文件

-S

只激活预处理和编译,就是指把文件编译成为汇编代码。
例子用法
  gcc -S hello.c
它将生成.s的汇编代码,你可以用文本编辑器察看

-E

只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面.
例子用法:
  gcc -E hello.c > pianoapan.txt
  gcc -E hello.c | more

段错误与coredump文件

Linux下C程序常常会因为内存访问错误等原因造成segment fault(段错误),此时如果系统core dump功能是打开的,那么将会有内存映像转储到硬盘上来,之后可以用gdb对core文件进行分析,还原系统发生段错误时刻的堆栈情况。这对于我们发现程序bug很有帮助。

core文件仅仅是一个内存映像(同时加上调试信息),主要是用来调试的。遇到某些无法处理的信号时会产生core文件。

使用ulimit -a可以查看系统core文件的大小限制;使用ulimit -c [kbytes]可以设置系统允许生成的core文件大小,例如:

ulimit -c 0 //不产生core文件
ulimit -c 100 //设置core文件最大为100k
ulimit -c unlimited //不限制core文件大小

g++ -g -o test test.c
./test
//发生段错误
gdb ./test core
(gdb) where
(gdb) r

分析内存泄漏

Valgrind通常用来成分析程序性能及程序中的内存泄露错误,使用到的是其中的memcheck,它用来检查程序中的内存问题,如泄漏、越界、非法指针等。
Valgrind包含下列工具:

1、memcheck:检查程序中的内存问题,如泄漏、越界、非法指针等。
2、callgrind:检测程序代码的运行时间和调用过程,以及分析程序性能。
3、cachegrind:分析CPU的cache命中率、丢失率,用于进行代码优化。
4、helgrind:用于检查多线程程序的竞态条件。
5、massif:堆栈分析器,指示程序中使用了多少堆内存等信息。
6、lackey:
7、nulgrind:

这几个工具的使用是通过命令:valgrand –tool=name 程序名来分别调用的,当不指定tool参数时默认是 –tool=memcheck。

Memcheck

最常用的工具,用来检测程序中出现的内存问题,所有对内存的读写都会被检测到,一切对malloc、free、new、delete的调用都会被捕获。所以,它能检测以下问题:
1、对未初始化内存的使用;
2、读/写释放后的内存块;
3、读/写超出malloc分配的内存块;
4、读/写不适当的栈中内存块;
5、内存泄漏,指向一块内存的指针永远丢失;
6、不正确的malloc/free或new/delete匹配;
7、memcpy()相关函数中的dst和src指针重叠。

这些问题往往是C/C++程序员最头疼的问题,Memcheck能在这里帮上大忙。

将程序编译生成可执行文件后执行:valgrind –leak-check=full ./程序名

动态链接过程

静态链接的缺点

(1)同一个模块被多个模块链接时,那么这个模块在磁盘和内存中都有多个副本,导致很大一部分空间被浪费了。
(2)当程序的任意一个模块发生更新时,整个程序都要重新链接、发布给用户。

动态链接的基本思想

动态就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。

链接文件

在Linux 中,ELF的动态链接文件被称为动态共享对象(DSO,dynamic shared object),以.so为扩展名的文件。
在WINDOWS中,EP的动态链接文件被称为动态链接库(DLL,dynamic link library),以 .dll 为扩展名的文件。

动态链接程序运行时地址空间的分布

在动态链接程序运行时,除了可执行文件本身,还有动态链接文件与动态链接器将被映射到进程的地址空间中,动态链接器被当做普通的共享对象来进行映射,在系统运行可执行文件之前,会将控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给可执行文件。

如何确定链接地址

编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成OBJ文件。
编译单元:根据C++标准,每一个CPP文件就是一个编译单元。每个编译单元之间是相互独立并且互相不可知。
目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据,还有一些其他信息,如未解决符号表导出符号表地址重定向表等。目标文件是以二进制的形式存在的。

根据C++标准,一个编译单元(Translation Unit)是指一个.cpp文件以及这所include的所有.h文件,.h文件里面的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE(Portable Executable,即Windows可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器链接成为一个.exe或.dll文件。

下面让我们来分析一下编译器的工作过程:
我们跳过语法分析,直接来到目标文件的生成,假设我们有一个A.cpp文件,如下定义:

    int n = 1;
    void FunA()
    {
        ++n;
    }

它编译出来的目标文件A.obj就会有一个区域(或者说是段),包含以上的数据和函数,其中就有n、FunA,以文件偏移量形式给出可能就是下面这种情况:

偏移量    内容    长度
0x0000    n       4
0x0004    FunA    ??

注意:这只是说明,与实际目标文件的布局可能不一样,??表示长度未知,目标文件的各个数据可能不是连续的,也不一定是从0x0000开始。
FunA函数的内容可能如下:

    0x0004 inc DWORD PTR[0x0000]
    0x00?? ret

这时++n已经被翻译成inc DWORD PTR[0x0000],也就是说把本单元0x0000位置的一个DWORD(4字节)加1。

有另外一个B.cpp文件,定义如下:

    extern int n;
    void FunB()
    {
        ++n;
    }

它对应的B.obj的二进制应该是:

偏移量    内容    长度
0x0000    FunB    ??

这里为什么没有n的空间呢,因为n被声明为extern,这个extern关键字就是告诉编译器n已经在别的编译单元里定义了,在这个单元里就不要定义了。由于编译单元之间是互不相关的,所以编译器就不知道n究竟在哪里,所以在函数FunB就没有办法生成n的地址,那么函数FunB中就是这样的:

    0x0000 inc DWORD PTR[????]
    0x00?? ret

那怎么办呢?这个工作就只能由链接器来完成了。

未解决符号表、导出符号表、地址重定义表

为了能让链接器知道哪些地方的地址没有填好(也就是还????),那么目标文件中就要有一个表来告诉链接器,这个表就是“未解决符号表”,也就是unresolved symbol table。同样,提供n的目标文件也要提供一个“导出符号表”也就是exprot symbol table,来告诉链接器自己可以提供哪些地址。

一个目标文件不仅要提供数据和二进制代码外,还至少要提供两个表:未解决符号表和导出符号表,来告诉链接器自己需要什么和自己能提供些什么。那么这两个表是怎么建立对应关系的呢?这里就有一个新的概念:符号。在C/C++中,每一个变量及函数都会有自己的符号,如变量n的符号就是n,函数的符号会更加复杂,假设FunA的符号就是_FunA(根据编译器不同而不同)。所以,
A.obj的导出符号表为

符号    地址
n       0x0000
_FunA   0x0004

未解决符号为空(因为他没有引用别的编译单元里的东西)。
B.obj的导出符号表为

符号    地址
_FunB   0x0000

未解决符号表为

符号    地址
n       0x0001

这个表告诉链接器,在本编译单元0x0001位置有一个地址,该地址不明,但符号是n。

在链接的时候,链接在B.obj中发现了未解决符号,就会在所有的编译单元中的导出符号表去查找与这个未解决符号相匹配的符号名,如果找到,就把这个符号的地址填到B.obj的未解决符号的地址处。如果没有找到,就会报链接错误。在此例中,在A.obj中会找到符号n,就会把n的地址填到B.obj的0x0001处。

但是,这里还会有一个问题,如果是这样的话,B.obj的函数FunB的内容就会变成inc DWORD PTR[0x000](因为n在A.obj中的地址是0x0000),由于每个编译单元的地址都是从0x0000开始,那么最终多个目标文件链接时就会导致地址重复。所以链接器在链接时就会对每个目标文件的地址进行调整。在这个例子中,假如B.obj的0x0000被定位到可执行文件的0x00001000上,而A.obj的0x0000被定位到可执行文件的0x00002000上,那么实现上对链接器来说,A.obj的导出符号地地址都会加上0x00002000,B.obj所有的符号地址也会加上0x00001000。这样就可以保证地址不会重复。

既然n的地址会加上0x00002000,那么FunA中的inc DWORD PTR[0x0000]就是错误的,所以目标文件还要提供一个表,叫地址重定向表,address redirect table。

总结一下:
目标文件至少要提供三个表:未解决符号表,导出符号表和地址重定向表。
未解决符号表:列出了本单元里有引用但是不在本单元定义的符号及其出现的地址。
导出符号表:提供了本编译单元具有定义,并且可以提供给其他编译单元使用的符号及其在本单元中的地址。
地址重定向表:提供了本编译单元所有对自身地址的引用记录。

链接器的工作顺序:
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
说明:实现链接的时候会更加复杂,一般实现的目标文件都会把数据,代码分成好向个区,重定向按区进行,但原理都是一样的。
明白了编译器与链接器的工作原理后,对于一些链接错误就容易解决了。

下面再看一看C/C++中提供的一些特性:
extern:这就是告诉编译器,这个变量或函数在别的编译单元里定义了,也就是要把这个符号放到未解决符号表里面去(外部链接)。

static:如果该关键字位于全局函数或者变量的声明前面,表明该编译单元不导出这个函数或变量,因些这个符号不能在别的编译单元中使用(内部链接)。如果是static局部变量,则该变量的存储方式和全局变量一样,但是仍然不导出符号。

默认链接属性:对于函数和变量,默认链接是外部链接,对于const变量,默认内部链接。

外部链接的利弊:外部链接的符号在整个程序范围内都是可以使用的,这就要求其他编译单元不能导出相同的符号(不然就会报duplicated external symbols)。
内部链接的利弊:内部链接的符号不能在别的编译单元中使用。但不同的编译单元可以拥有同样的名称的符号。

为什么头文件里一般只可以有声明不能有定义:头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external symbols链接错误。

为什么公共使用的内联函数要定义于头文件里:因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。

数组和指针

数组名具有普通变量的直接性,它的地址就是实际数组首元素的地址。因此它不用取两次地址里的内容而是取一次,同时又具有和指针相同的偏移量引用方式,即下标的实现实际是由指针加偏移量实现的。

下标操作中的表现

C语言对指针与数组的引用方式做了可以“交叉”使用的语法规定。就上面的例子来说,如果p指针指向数组b时,b[i]、(b+i)、p[i]、(p+i)都是对数组第i个元素的正确引用方式,这也给很多C语言学习者制造了“指针和数组一样”的错觉。

在函数形参中的表现

在向函数传递参数时,如果实参是一个一维数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组。这么做的原因主要是效率,这是无可争议的,但为了使用上的简便与通用,C语言接受两种形参的写法,即下面3种写法是相同的

int foo(int *a, int n);  
int foo(int a[], int n); 
int foo(int a[20], int n);  

迭代器失效问题

list,set,map容器

题目: 删除map

void deleteValueFromMap(Map &m, int n = 5)
{
     MapIt it;
     for(it = m.begin(); it != m.end(); /*不能再自增了*/)
     {
         if(0 == it->second % n)
         {
             m.erase(it++);
         }
         else
         {
             it++;
         }
     }
 }

对于关联的容器map来说, m.erase(it)后,it就失效了,所以要用m.erase(it++)。这样一来,it++先执行, 指向了下一个元素,同时返回的还是当前元素, 此时再删除它就没有问题了。

或者:

std::list< int> List;
std::list< int>::iterator itList;
for( itList = List.begin(); itList != List.end(); )
{
      if( WillDelete( *itList) )
      {
            itList = List.erase( itList);
       }
       else
            itList++;
}

这里通过erase方法的返回值也可以获取下一个元素的位置。

vector,deque容器

而vector中则要这样处理:

void deleteValueFromVector(Vec &v, int n = 5)
{
     VecIt it;
     for(it = v.begin(); it != v.end(); /*不能再自增了*/)
     {
         if(0 == *it % n)
         {
             it = v.erase(it); // erase的返回值就是指向被删除的元素的下一个元素的迭代器
         }
         else
         {
             it++;
         }
     }
}

读写锁的实现

使用互斥锁和条件变量实现读写锁:

class readwrite_lock
{
public:
    readwrite_lock(): stat(0){}

    void readLock()
    {
        mtx.lock();
        while (stat < 0)
            cond.wait(mtx);
        ++stat;
        mtx.unlock();
    }

    void readUnlock()
    {
        mtx.lock();
        if (--stat == 0)
            cond.notify_one(); // 叫醒一个等待的写操作
        mtx.unlock();
    }

    void writeLock()
    {
        mtx.lock();
        while (stat != 0)
            cond.wait(mtx);
        stat = -1;
        mtx.unlock();
    }

    void writeUnlock()
    {
        mtx.lock();
        stat = 0;
        cond.notify_all(); // 叫醒所有等待的读和写操作
        mtx.unlock();
    }

private:
    mutex mtx;
    condition_variable cond;
    int stat; // == 0 无锁;> 0 已加读锁个数;< 0 已加写锁
};

使用2个互斥锁实现读写锁:

class readwrite_lock
{
public:
    readwrite_lock():read_cnt(0){}

    void readLock()
    {
        read_mtx.lock();
        if (++read_cnt == 1)
            write_mtx.lock();

        read_mtx.unlock();
    }

    void readUnlock()
    {
        read_mtx.lock();
        if (--read_cnt == 0)
            write_mtx.unlock();

        read_mtx.unlock();
    }

    void writeLock()
    {
        write_mtx.lock();
    }

    void writeUnlock()
    {
        write_mtx.unlock();
    }

private:
    mutex read_mtx;
    mutex write_mtx;
    int read_cnt; // 已加读锁个数
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值