一、常量与宏回顾
C++中的const常量可以替代宏常数定义,如:
const int A = 3; -> #define A 3
C++中是否有解决方案替代宏代码片段呢?替代宏代码片段就可以避免宏的副作用!
二、C++中的函数
1. 内联函数
1. C++中推荐使用内联函数替代宏代码片段;
2. C++中使用inline关键字声明内联函数;
//3-1.cpp
#include <stdio.h>
#define FUNC(a, b) ((a) < (b) ? (a) : (b))
inline int func(int a, int b)
{
return a < b ? a : b;
}
int main(int argc, char *argv[])
{
int a = 1;
int b = 3;
int c = func(++a, b);
//int c = FUNC(++a, b);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果如下:
注:内联函数声明时inline关键字必须和函数定义结合在一起,否则编译器会直接忽略内联请求。下面的demo中func函数就一定不会是内联函数。
#include <stdio.h>
inline int func(int a, int b);
int main(int argc, char *argv[])
{
int a = 1;
int b = 3;
int c = func(++a, b);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
int func(int a, int b)
{
return a < b ? a : b;
}
内联函数的定义以及和普通函数的区别:
1. C++编译器可以将一个函数进行内联编译;
2. 被C++编译器内联编译的函数叫做内联函数;
3. 内联函数在最终生成的代码中是没有定义的;
4. C++编译器直接将函数体插入函数调用的地方;
5. 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回);
注:C++编译器不一定准许函数的内联请求!
内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等);
内联函数是对编译器的一种请求,因此编译器可能拒绝这种请求;
内联函数由编译器处理,直接将编译后的函数体插入调用的地方;
宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程;
现代C++编译器能够进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译;另外,一些现代C++编译器提供了扩展语法,能够对函数进行强制内联;如:g++中的__attribute__((always_inline))属性。
//main.cpp
#include <stdio.h>
inline int f_inline(int a, int b);
int g_no_inline(int a, int b);
int main(int argc, char *argv[])
{
int r1 = f_inline(1, 2);
int r2 = g_no_inline(1, 2);
printf("Press enter to continue ...");
getchar();
return 0;
}
int f_inline(int a, int b)
{
return a < b ? a : b;
}
int g_no_inline(int a, int b)
{
return a < b ? a : b;
}
通过下面的命令单步编译main.cpp,将产生一个main.s的汇编文件。
g++ -S main.cpp -o main_no_inline.s
#main_no_inline.s
.file "main.cpp"
.section .text$getchar,"x"
.linkonce discard
.globl _getchar
.def _getchar; .scl 2; .type 32; .endef
_getchar:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
movl __imp___iob, %eax
movl 4(%eax), %eax
leal -1(%eax), %edx
movl __imp___iob, %eax
movl %edx, 4(%eax)
movl __imp___iob, %eax
movl 4(%eax), %eax
testl %eax, %eax
js L2
movl __imp___iob, %eax
movl (%eax), %edx
movb (%edx), %al
movzbl %al, %eax
leal 1(%edx), %ecx
movl __imp___iob, %edx
movl %ecx, (%edx)
jmp L3
L2:
movl __imp___iob, %eax
movl %eax, (%esp)
call __filbuf
L3:
leave
ret
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "Press enter to continue ...\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $2, 4(%esp)
movl $1, (%esp)
call __Z8f_inlineii
movl %eax, 28(%esp)
movl $2, 4(%esp)
movl $1, (%esp)
call __Z11g_no_inlineii
movl %eax, 24(%esp)
movl $LC0, (%esp)
call _printf
call _getchar
movl $0, %eax
leave
ret
.section .text$_Z8f_inlineii,"x"
.linkonce discard
.globl __Z8f_inlineii
.def __Z8f_inlineii; .scl 2; .type 32; .endef
__Z8f_inlineii:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jge L6
movl 8(%ebp), %eax
jmp L7
L6:
movl 12(%ebp), %eax
L7:
popl %ebp
ret
.text
.globl __Z11g_no_inlineii
.def __Z11g_no_inlineii; .scl 2; .type 32; .endef
__Z11g_no_inlineii:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jge L9
movl 8(%ebp), %eax
jmp L10
L9:
movl 12(%ebp), %eax
L10:
popl %ebp
ret
.def __filbuf; .scl 2; .type 32; .endef
.def _printf; .scl 2; .type 32; .endef
通过分析main_no_inline.s发现,f_inline函数有普通函数调用时的额外开销(压栈,跳转,返回),即内联请求没有被满足。我们可以通过g++中扩展的属性进行强制的内联。
//main.cpp
#include <stdio.h>
inline int f_inline(int a, int b) __attribute__((always_inline));
int g_no_inline(int a, int b);
int main(int argc, char *argv[])
{
int r1 = f_inline(1, 2);
int r2 = g_no_inline(1, 2);
printf("Press enter to continue ...");
getchar();
return 0;
}
int f_inline(int a, int b)
{
return a < b ? a : b;
}
int g_no_inline(int a, int b)
{
return a < b ? a : b;
}
通过命令进行编译:g++ -S main.cpp -o main_inline.s
#main_inline.s
.file "main.cpp"
.section .text$getchar,"x"
.linkonce discard
.globl _getchar
.def _getchar; .scl 2; .type 32; .endef
_getchar:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
movl __imp___iob, %eax
movl 4(%eax), %eax
leal -1(%eax), %edx
movl __imp___iob, %eax
movl %edx, 4(%eax)
movl __imp___iob, %eax
movl 4(%eax), %eax
testl %eax, %eax
js L2
movl __imp___iob, %eax
movl (%eax), %edx
movb (%edx), %al
movzbl %al, %eax
leal 1(%edx), %ecx
movl __imp___iob, %edx
movl %ecx, (%edx)
jmp L3
L2:
movl __imp___iob, %eax
movl %eax, (%esp)
call __filbuf
L3:
leave
ret
.def ___main; .scl 2; .type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "Press enter to continue ...\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $1, 20(%esp)
movl $2, 16(%esp)
movl 20(%esp), %eax
cmpl 16(%esp), %eax
jge L5
movl 20(%esp), %eax
jmp L6
L5:
movl 16(%esp), %eax
L6:
movl %eax, 28(%esp)
movl $2, 4(%esp)
movl $1, (%esp)
call __Z11g_no_inlineii
movl %eax, 24(%esp)
movl $LC0, (%esp)
call _printf
call _getchar
movl $0, %eax
leave
ret
.globl __Z11g_no_inlineii
.def __Z11g_no_inlineii; .scl 2; .type 32; .endef
__Z11g_no_inlineii:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jge L8
movl 8(%ebp), %eax
jmp L9
L8:
movl 12(%ebp), %eax
L9:
popl %ebp
ret
.def __filbuf; .scl 2; .type 32; .endef
.def _printf; .scl 2; .type 32; .endef
通过分析main_inline.s汇编文件发现,此时并没有发现f_inline函数。这就说明了f_inline函数被内联编译了。前面有讲到过C++编译器直接将内联函数的函数体插入函数调用的地方。main_inline.s中哪些汇编代码又是f_inline函数的函数体呢?下面的汇编代码就是f_inline函数的函数体。
movl $1, 20(%esp)
movl $2, 16(%esp)
movl 20(%esp), %eax
cmpl 16(%esp), %eax
jge L5
movl 20(%esp), %eax
jmp L6
C++中内联编译的限制:
1. 不能存在任何形式的循环语句
2. 不能存在过多的条件判断语句
3. 函数体不能过于庞大
4. 不能对函数进行取址操作
5. 函数内联声明必须在调用语句之前
编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈,跳转和返回的开销。因此,当函数体的执行开销远大于压栈,跳转和返回所用的开销时,那么内联将无意义。
C++中内联函数的实现机制
当C++编译器编译程序,遇到通过inline关键字声明的函数的时候,就明白该函数在请求内联编译。因此C++编译器就会检查该函数体,如果符合内联函数的要求,C++编译器就会将它加入到C++编译器自己的符号表中。符号表是C++编译器编译程序的时候生成的一张表,是编译器自己用的。编译生成的可执行文件中没有包含符号表的信息。当C++编译器遇到函数调用的时候,首先进行函数参数类型的检查,当发现符号表中有对应的函数时,就会将符号表中的函数体替换到函数调用的位置。这也说明了,当C++编译内联函数后,在生成的汇编代码中看不到内联函数的声明以及内联函数定义,只有内联函数的函数体。
2. 函数默认参数
C++中可以在函数声明时为参数提供一个默认值,当函数调用时没有指定这个参数的值,编译器会自动用默认值代替。
#include <stdio.h>
int mul(int x = 3);
int main(int argc, char *argv[])
{
printf("mul(2) = %d\n", mul(2));
printf("mul(-2) = %d\n", mul(-2));
printf("mul() = %d\n", mul());
printf("Press enter to continue ...");
getchar();
return 0;
}
int mul(int x)
{
return x * x;
}
运行结果如下:
- 函数定义中是否可以出现参数的默认值? 编译器报错
- 当函数声明和定义中的参数默认值不同时会发生什么? 编译器报错
函数默认参数的规则
1. 只有参数列表后面部分的参数才可以提供默认参数值;
2. 一旦在一个函数调用中开始使用默认参数值,那么这个参数后的所有参数都必须使用默认参数值;
#include <stdio.h>
int add(int a, int b = 0, int c = 0, int d = 0)
{
return a + b + c;
}
int main(int argc, char *argv[])
{
printf("add(2) = %d\n", add(2));
printf("add(1, 2) = %d\n", add(1, 2));
printf("add(1, 2, 3) = %d\n", add(1, 2, 3));
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果如下:
3. C++中函数占位参数
在C++中可以为函数提供占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
#include <stdio.h>
int func(int a, int b, int)
{
return a + b;
}
int main(int argc, char *argv[])
{
printf("func(1, 2, 3) = %d\n", func(1, 2, 3));//必须传递三个参数
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果如下:
C++支持这样的函数占位参数有什么意义?
可以将占位参数与默认参数结合起来使用
意义
1. 为以后程序的扩展留下线索
2. 兼容C语言程序中可能出现的不规范写法
在C语言中不等价。void func();表示可以接受任意多个的参数。void func(void);表示不接受参数。
C++中等价。
#include <stdio.h>
int func(int a, int b, int = 0)//当看到int = 0,就说明这个函数以后可能会修改
{
return a + b;
}
int main(int argc, char *argv[])
{
printf("func(1, 2) = %d\n", func(1, 2));
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果如下:
下面我们再看一个例子,说明兼容C语言程序中可能出现的不规范写法的程序。
#include <stdio.h>
int func()
{
return 1;
}
int main(int argc, char *argv[])
{
printf("func() = %d\n", func());
printf("func(1) = %d\n", func(1));
printf("Press enter to continue ...");
getchar();
return 0;
}
因为C语言中,定义一个函数如果没有任何参数,默认是可以传递任意个实参的。因此上述程序通过C编译器编译可以通过。
同样的程序,通过C++编译呢?
通过C++编译器编译,就一个地方报错,我们修改报错的地方,不在传入参数就好,但如果有几十个地方或者上百个地方都调用了呢?C++为了兼容C代码,提供了函数占位符参数。我们只要修改函数定义的地方就能正常编译运行了,不需要改多个地方。
#include <stdio.h>
int func(int = 0)
{
return 1;
}
int main(int argc, char *argv[])
{
printf("func() = %d\n", func());
printf("func(1) = %d\n", func(1));
printf("Press enter to continue ...");
getchar();
return 0;
}
编译运行结果如下:
小结:
1. C++中可以通过inline声明内联函数
2. 内联函数在编译时直接将函数体插入函数调用的地方
3. inline只是一种请求,编译器不一定允许这种请求
4. 内联函数省去了普通函数调用时压栈,跳转和返回的开销
5. C++中在声明函数的时候指定参数的默认值
6. C++可以声明占位符参数,占位符参数一般用于程序扩展和对C代码的兼容
4. C++中的函数重载
函数重载(Function Overload),用同一个函数名定义不同的函数,当函数名和不同的参数搭配时函数的含义不同。
#include <stdio.h>
#include <string.h>
int func(int x)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
int main(int argc, char *argv[])
{
int c = 0;
c = func(1);
printf("c = %d\n", c);
c = func(1, 2);
printf("c = %d\n", c);
c = func("12345");
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果:
函数重载至少满足下面的一个条件:
1. 参数个数不同
2. 参数类型不同
3. 参数顺序不同
#include <stdio.h>
#include <string.h>
int func(int x)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
int func(int a, const char* s)
{
return a;
}
int func(const char* s, int a)
{
return strlen(s);
}
int main(int argc, char *argv[])
{
int c = 0;
c = func("ab", 1);
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果如下:
当函数默认参数遇上函数重载会发生什么?
#include <stdio.h>
#include <string.h>
int func(int a, int b, int c = 0)
{
return a * b * c;
}
int func(int a, int b)
{
return a + b;
}
int main(int argc, char *argv[])
{
int c = 0;
c = func(1, 2); // 存在二义性,调用失败,编译不能通过
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果如下:
编译器调用重载函数的准则:
将所有同名函数作为候选者
尝试寻找可行的候选函数
1. 精确匹配实参;
2. 通过默认参数能够匹配实参;
3. 通过默认类型转换匹配实参;
匹配失败
1. 最终寻找到的可行候选函数不唯一,则出现二义性,编译失败。
2. 无法匹配所有候选者,函数未定义,编译失败。
函数重载的注意事项:
1. 重载函数在本质上是相互独立的不同函数;
2. 重载函数的函数类型是不同的;
3. 函数返回值不能作为函数重载的依据;
注:函数重载是由函数名和参数列表决定的。
函数重载与函数指针
当使用重载函数名对函数指针进行赋值时
1. 根据重载规则挑选与函数指针参数列表一致的候选者
2. 严格匹配候选者的函数类型与函数指针的函数类型(包括返回值类型和参数类型)
#include <stdio.h>
#include <string.h>
int func(int x) // int(int a)
{
return x;
}
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
typedef int(*PFUNC)(int a); // int(int a)
int main(int argc, char *argv[])
{
int c = 0;
PFUNC p = func;
c = p(1);
printf("c = %d\n", c);
printf("Press enter to continue ...");
getchar();
return 0;
}
上面程序中哪个函数将被调用?运行结果如下:
注意:
- 函数重载必然发生在同一个作用域中;
- 编译器需要用参数列表或函数类型进行函数选择;
- 无法直接通过函数名得到重载函数的入口地址;
三、C++和C的相互调用
在项目中融合C++和C代码是实际工程中不可避免的。虽然C++编译器能够兼容C语言的编译方式,但C++编译器会优先使用C++的方式进行编译。可以利用extern关键字强制让C++编译器对代码进行C方式编译。
1. C++调用C编写的函数
//main.cpp
#include <stdio.h>
extern "C"
{
#include "add.h"
}
int main()
{
printf("1 + 2 = %d\n", add(1, 2));
return 0;
}
//add.c
#include "add.h"
int add(int a, int b)
{
return a + b;
}
//add.h
int add(int a, int b);
通过下面的命令进行编译运行:
2. C调用C++编写的函数
//main.c
#include <stdio.h>
#include "add.h"
int main()
{
printf("1 + 2 = %d\n", add(1, 2));
return 0;
}
//add.cpp
extern "C"
{
#include "add.h"
int add(int a, int b)
{
return a + b;
}
}
//add.h
int add(int a, int b);
通过下面的命令进行编译运行:
统一的解决方案
__cplusplus是C++编译器内置的标准宏定义
__cplusplus的意义:让C代码即可以通过C编译器的编译,也可以在C++编译器中以C方式编译
#ifdef __cplusplus
extern "C" {
#endif
//函数声明或函数定义
#ifdef __cplusplus
}
#endif
注:C++编译器不能以C的方式编译多个重载函数
#include <stdio.h>
#include <string.h>
#ifdef __cplusplus
extern "C" {
#endif
int func(int a, int b)
{
return a + b;
}
int func(const char* s)
{
return strlen(s);
}
#ifdef __cplusplus
}
#endif
int main(int argc, char *argv[])
{
printf("Press enter to continue ...");
getchar();
return 0;
}
运行结果: