86.define、const、typedef、inline的使用方法?他们之间有什么区别?

86.define、const、typedef、inline的使用方法?他们之间有什么区别?

一、const与#define的区别:

  1. const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
  2. define只在预处理阶段起作用,简单的文本替换,而const在编译、链接过程中起作用;
  3. define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
  4. define预处理后,占用代码段空间,const占用数据段空间;
  5. const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
  6. define独特功能,比如可以用来防止文件重复引用。

二、#define和别名typedef的区别

  1. 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;
  2. 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
  3. 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

三、 define与inline的区别

  1. #define是关键字,inline是函数;
  2. 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
  3. inline函数有类型检查,相比宏定义比较安全;

1.C++之define详解

●“#”表示其为预处理命令,凡是以“#”开头的都是预处理命令;“define”为宏定义命令;“标识符”为所定义的宏名;“字符串”可以是常数、表达式、字符串等。

●define只是做简单的文本替换,经常用到的如“#define MAXNUM 100”,带有表达式如“#define M (x+y)”,注意,的表达式中()是必须的,否则在进行如“2*M+2”的运算 时就会出现错误,末尾不需要分号(如果加分号,会连同分号一起代换)。

●作用域为宏定义开始,到源程序结束,终止定义域可用“#undef M”(如果想要取消名为“M”的宏)。

●宏名如果以字符串的形式被调用,则不做替换,如printf(“I M O”)。

●可以嵌套进行定义,如:#define PI 3.14  #define S PIRR

●习惯上把宏名写为大写的,以区别普通的变量。

●宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。

●可用宏定义表示数据类型,使书写方便。

●带参宏定义中,宏名和形参表之间不能有空格出现。

●在宏定义中的形参是标识符,而宏调用中的实参可以是表达式

●预处理命令:

#//空指令,无任何效果
#include//包含一个源代码文件
#define//定义宏
#undef//取消已定义的宏
#if//如果给定条件为真,则编译下面代码
#ifdef//如果宏已经定义,则编译下面代码
#ifndef//如果宏没有定义,则编译下面代码
#elif//如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif//结束一个#if……#else条件编译块
#error//停止编译并显示错误信息

————————————————
版权声明:本文为CSDN博主「icewst」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/icewst/article/details/103610590

1.1简单的define定义

#define MAXTIME 1000
if(i<MAXTIME){.........}

一个简单的MAXTIME就定义好了,它代表1000,如果在程序里面写

if(i<MAXTIME){.........}

编译器在处理这个代码之前会对MAXTIME进行处理替换为1000。

这样的定义看起来类似于普通的常量定义CONST,但也有着不同,因为define的定义更像是简单的文本替换,而不是作为一个量来使用。

1.2define的“函数定义”

define可以像函数那样接受一些参数,如下

#define max(x,y) (x)>(y)?(x):(y);

这个定义就将返回两个数中较大的那个,看到了吗?因为这个“函数”没有类型检查,就好像一个函数模板似的,当然,它绝对没有模板那么安全就是了。可以作为一个简单的模板来使用而已。

但是这样做的话存在隐患,例子如下:

#define Add(a,b) a+b;

在一般使用的时候是没有问题的,但是如果遇到如:c * Add(a,b) * d的时候就会出现问题,代数式的本意是a+b然后去和c,d相乘,但是因为使用了define(它只是一个简单的替换),所以式子实际上变成了

ca + bd

(1)宏名和形参之间不能有空格。如果上式写为 #define MAX (a,b) (a>b)?a:b,则MAX就表示整个后面的部分了。

(2)带参宏定义的形参是不分配内存的。

(3) 在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。

define可以替代多行的代码,例如MFC中的宏定义(非常的经典,虽然让人看了恶心)

#define SWAP(a,b) do\
{\
decltype(a) temp = a;\
a = b;\
b = temp;\
}while(0)

●函数定义块如果需要换行,那么换行是结尾需加反斜杠

●可以利用decltype来获得函数参数的类型,方便函数中内容的执行

●利用do while(0)可以使函数中的变量变成局部变量,且使语法清晰减少出错

●有时可用这种宏的方式可以代替c++的模板,执行效率要比模板快

●因为是文本替换,所以尽量不要把分号写进去,在调用的时候补充分号

1.3在大规模的开发过程中,特别是跨平台和系统的软件里,define最重要的功能是条件编译。

可以在编译的时候通过#define设置编译环境

//不同的运行环境,不同的头文件
#ifdef OS_Win
#include <windows.h>
#endif

#ifdef OS_Linux
#include <linux.h>
#endif

1.4如何定义宏、取消宏

//定义宏
#define [MacroName] [MacroValue]
//取消宏
#undef [MacroName]
普通宏
#define PI (3.1415926)

带参数的宏
#define max(a,b) ((a)>(b)? (a),(b))
关键是十分容易产生错误,包括机器和人理解上的差异等等。

1.5条件编译

#ifdef XXX…(#else) …#endif
例如 #ifdef DV22_AUX_INPUT
#define AUX_MODE 3
#else
#define AUY_MODE 3
#endif
#ifndef XXX … (#else) … #endif

1.6头文件(.h)可以被头文件或C文件包含;

重复包含(重复定义)
由于头文件包含可以嵌套,那么C文件就有可能包含多次同一个头文件,就可能出现重复定义的问题的。
通过条件编译开关来避免重复包含(重复定义)
例如

#ifndef headerfileXXX
#define headerfileXXX
…
文件内容
…
#endif

1.7define和typedef的区别

define宏定义是在预处理完成的,typedef是在编译时处理的,typedef不是简单的代换,而是对类型说明符的重命名。

例如:

#define P1 int*
typedef int* P2;
P1 a, b;//相当于int* a, b,此时a是int指针,b是int整数。
P2 a, b;//表示a和b都是int指针。

1.8宏定义中的特殊操作符

define 中的特殊操作符有#,##和… and VA_ARGS

1)#

#是字符串化的意思,出现在宏定义中的#是把跟在后面的参数转成一个字符串;

例如:

#include <stdio.h>
#define PSQR(x) printf("the square of " #x " is %d.\n",(x)*(x))
int main(void)
{
    int y = 4;
    PSQR(y);
    //输出:the square of y is 16.
    PSQR(2 + 4);
    //输出:the square of 2+4 is 36.
    return 0;
}

例如:

#define  strcpy__(dst, src)      strcpy(dst, #src)

strcpy__(buff,abc) 相当于 strcpy(buff,“abc”)

2)##

##是连接符号,把参数连接在一起

#define FUN(arg)     my##arg

 FUN(ABC)

等价于

 myABC

使用

#include <iostream>
using namespace std;
#define B(exp) cout << sz##exp << endl;

int main(void)
{
    char* szStr = (char*)"test";
    B(Str);     // szStr 
    return 0;
}

3)可变参数宏 …和__VA_ARGS__

VA_ARGS 是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。
实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏__VA_ARGS__就可以被用在替换部分中,替换省略号所代表的字符串。

例如:

#define PR(...) printf(__VA_ARGS__)
int main()
{
    int wt=1,sp=2;
    PR("hello\n");
    //输出:hello
    PR("weight = %d, shipping = %d",wt,sp);
    //输出:weight = 1, shipping = 2
    return 0;
}

省略号只能代替最后面的宏参数。
#define W(x,…,y)错误!
但是支持#define W(x, …),此时传入的参数个数必须能够匹配。

这里再介绍几个系统的宏:
●_VA_ARGS_ 是一个可变参数的宏,很少人知道这个宏,这个可变参数的宏是新的C99规范中新增的,目前似乎只有gcc支持(VC6.0的编译器不支持)。宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的”,”去掉的作用,否则会编译出错, 你可以试试。
●__FILE__ 宏在预编译时会替换成当前的源文件名
●__LINE__宏在预编译时会替换成当前的行号
●__FUNCTION__宏在预编译时会替换成当前的函数名称

————————————————
版权声明:本文为CSDN博主「icewst」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/icewst/article/details/103610590

  编写程序过程中,我们有时不希望改变某个变量的值。此时就可以使用关键字 const 对变量的类型加以限定。

初始化和const

  因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。一如既往,初始值可以是任意复杂的表达式:

const int i = get_size();//正确:运行时初始化
const int j = 42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量 

  正如之前反复提到的,对象的类型决定了其上的操作。与非const类型所能参与的操作相比,const类型的对象能完成其中大部分,但也不是所有的操作都适合。主要的限制就是只能在const类型的对象上执行不改变其内容的操作。例如,const int和普通的int一样都能参与算术运算,也都能转换成一个布尔值,等等 。
在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外 一个对象,则它们是不是const都无关紧要:

int i = 42;
const int ci = i;//正确: 1的值被拷贝给了ci
int j = ci;//正确:ci的值被拷贝给了J

默认状态下,const对象仅在文件内有效

const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。

  当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:

const int bufSize = 512;//输入缓冲区大小

  编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。为了执行上述替换,编译器必须知道变量的初始值 。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义(参见C++Primer 2.2.2节,第41页)。为了支持这一用法, 同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
  某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中 定义const,而在其他多个文件中声明并使用它。
  解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:

//file_l.c定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize =fen();
//file_l.h头文件
extern const int bufSize;//与file_l.c中定义的bufSize是同一个
假设我们有两个文件:main.cpp和helper.cpp。我们在main.cpp中定义了一个全局变量,并使用const关键字进行修饰:
```cpp
// main.cpp
const int MAX_VALUE = 100;
int main() 
{
    // 使用MAX_VALUE
    return 0;
}
```
然后我们在helper.cpp中尝试引用这个变量:
```cpp
// helper.cpp
extern const int MAX_VALUE;
void helperFunction()
{
    // 使用MAX_VALUE
}
```
这时候编译器会报错,提示MAX_VALUE未定义。这是因为const修饰的变量默认具有内部链接(internal linkage),只能在当前文件中使用,其他文件无法访问。因此,我们需要在helper.cpp中重新定义MAX_VALUE:

```cpp
// helper.cpp
const int MAX_VALUE = 100;
void helperFunction() 
{
    // 使用MAX_VALUE
}
```
这样就可以在helper.cpp中使用MAX_VALUE了。注意,这里重新定义的MAX_VALUE和main.cpp中定义的MAX_VALUE虽然名字相同,但是它们是两个不同的变量,它们的地址也不同。
const修饰变量的隐藏作用是只能在该文件中使用,其他文件无法引用声明使用。如果想在其他文件中使用这个const修饰的变量,可以通过将该变量的定义放在头文件中,然后在其他文件中包含该头文件来实现。

下面是一个例子:
// header.h
#ifndef HEADER_H
#define HEADER_H
const int MAX_VALUE = 100;
#endif // HEADER_H

// file1.cpp
#include "header.h"
int main() 
{
    const int value = MAX_VALUE;
    // ...
}

// file2.cpp
#include "header.h"
int main() 
{
    int value = MAX_VALUE;
    // ...
}

2.C++中的const

1.变量中的const

1.1 普通变量

直接在普通变量类型声明符前加上const,可以将其声明为const类型:

const int a = 0;

这样就把a声明成了一个const类型的常量,所以我们不能再改变它的值了,所以下面试图改变a的语句将会编译报错:

a = 10;

修改局部变量的值:

1.如果const修饰的局部变量是基础的类型(int char double等等),并且初始化使用字面常量的话,不会给该变量分配空间。
例如:

void test()
{
	const int a = 10;//用字面常量10来初始化
	a = 20;//error
}

2.但是,当我们对这个变量进行取地址的操作的时候,系统会为该变量分配空间。

void test() 
{
	const int a = 10;
	//a = 20;//error
	int* p = (int*)&a;
	*p = 20;
	cout << a << endl;
	cout << *p << endl;
}

上面的结果是:10和20

  这是因为,当我们定义一个被const修饰并且使用字面常量来初始化的局部变量的时候,系统会把这个变量看作是一个符号,放入到符号表中,这么变量名就是一个符号,值就是这个符号的值,类似于#define的作用。(这就是 C++ 中的常量折叠 ,因为常量是在运行时初始化的,编译器对常量进行优化,直接将常量值放在编译器的符号表中,使用常量时直接从符号表中取出常量的值,省去了访存这一步骤。)

  当我们对这个变量取地址的时候,由于原来没有空间,就没有地址,现在需要取地址,所以才被迫分配一块空间,我们通过地址的解引用可以修改这个空间的值,这也就是为什么第二个结果为20的原因,但是如果我们还是通过变量名来访问数据的话,系统会认为这还是一个符号,直接用符号表里面的值替换。

但是!

3.如果初始化不是用字面常量而是用变量,那么系统会直接分配空间。

void test() 
{
	int b = 20;
	const int a = b;
}

这时候的a是有空间的,不会被放入到符号表中。

修改全局变量的值

  通过指针修改位于静态存储区的的const变量,语法上没有报错,编译不会出错,一旦运行就会报告异常。因为全局变量存储于静态存储区,静态存储区中的常量只有读权限,不能修改它的值。

  与C一样,当const修饰普通的全局变量的时候,不能通过变量名和地址来修改变量的值。

另外

  与C不一样的是,C语言中的const修饰的普通全局变量默认是外部链接属性的,但是在C++中被const修饰的普通全局变量是内部链接属性的。

  也就是说当我们在一个文件中定义了一个如下的全局变量

const int a = 10;//定义全局变量

int main() 
{
	return 0;
}

  我们在另外一个文件中,使用extern来声明,也是不可以的。

//另外一个文件

extern const int a;//在另外的文件中声明

  上面这种做法是不可以的,C++中被const修饰的全局变量默认是内部链接属性,不能直接在另外的文件中使用,如果想要在另外的文件中使用,就需要在定义该全局的变量的文件中用extern来修饰(另一个文件也需要extern修饰)。

//定义的文件
extern const int a = 10;
//另外一个文件声明
extern const int a;

原文链接:https://blog.csdn.net/weixin_61021362/article/details/121544469

1.2 const 修饰引用

  我们还可以对引用使用const限定符,在引用声明的类型声明符前加上const就可以声明对const的引用,常量引用不能用来修改它所绑定的对象。

引用绑定到同一种类型,并修改值

例子:

int i = 0;
const int j = 0;
const int &r1 = i;
//r1 = 20;//err不能给常量赋值	
const int &r2 = j;
//r2 = 20;//err不能给常量赋值	
int &r3 = j;

  第三行将非常量对象 i 绑定到 const 引用 r1 上,此过程中发生了隐式类型转换,i 的类型为 int,r1 的类型为 const int &, 所以这个过程 i 就从 int 转换为了 const int,所以不能通过 r1 改变 i 的值,但可以直接改变 i 的值。但是 const int 类型不能转换为 int。

可以这样理解:const int是int的一种,但是范围更小,将int限定在一个范围之类,(本身int = const int类型 + 非const类型),没有问题。但是const int到int范围扩大,超出权限。

  第五行将常量对象 j 绑定到 const 引用 r2 上,不能直接改变 j 的值也不能通过常量引用改变 j 的值。
  第七行将常量对象绑定到 const 引用 r3 上,报错,不能将常量对象绑定到常量引用上。

绑定到另一种类型,并修改值

例子:

void func()
{
	double i = 1.0;
	const int& r1 = i;
	i = 2.0;
	cout << "i = " << i << endl;
	cout << "r1 = " << r1 << endl;

	double j = 1.0;
	const double& r2 = j;
	j = 2.0;
	//r2 = 3.0;//err不能通过r2修改j的值
	cout << "j = " << j << endl;
	cout << "r2 = " << r2 << endl;
}
---------------------------------------
out:
i = 2
r1 = 1
j = 2
r2 = 2

  上面的代码将 int 型的引用 r1 绑定到 double 型变量 i 上,然后改变 i 的值,我们发现 r1 并没有改变,它的值反而是绑定 i 时 i 的值。这是因为引用变量的类型与被引用对象的类型不同时,中间会有如下操作:

double i = 1.0;
int temp = i;
const int &r1 = temp;

  r1 引用的是临时量 temp,而不是 i,所以才会出现上面的情况。

原文链接:https://blog.csdn.net/weixin_45773137/article/details/126297568

1.3 const 修饰指针

  当使用const修饰指针变量时,情况就复杂起来了。const可以放置在不同的地方,因此具有不同的含义。来看下面一个例子:

int age = 39;
const int * p1 = &age;
int const * p2 = &age;
int * const p3 = &age;
const int * const p4 = &age;

  二三行是一个意思,表示 p1和p2是指向常量的指针;第四行表示 p3是常量指针;第五行表示p4是指向常量的常量指针。
  上面二三行的赋值同样发生了类型转换,从 int * 转换为 const int *。

常量指针和常量指针

指向常量的指针变量——const int* p;//p的内容(即值,也就是它指向的东西的地址)是可以改变的;*的左边是const int,表明p指向的东西是一个const的int,我们不能通过p来修改这个int,因为它是const的。

指向变量的指针常量——int* const p;

指向常量的指针常量——const int* const p;
void func()
{
	const int i1 = 1;
	int i2 = 2;

	const int j1 = 3;
	int j2 = 4;

	const int k1 = 5;
	int k2 = 6;

	//const int *p和int const *p同样含义,原因:const修饰的是后面跟的整体
	const int* p1 = &i1;//指向常量的指针变量   const后跟了*和p1,(*p1)不可修改(同时p1指向的变量也要求不能修改)
	//i1 = 2;//err常量i1是常量不能通过自身修改
	//*p1 = 2;//err p1指向的常量i1是常量不能通过自身修改
	p1 = &i2;//p1的指向可以修改,可以将非const变量赋值给const修饰的变量(压缩非const的操作)
	cout << *p1 << endl;

	int* const p2 = &j2;//指向变量的指针常量
	//p2 = j2;//错误
	//p2 = k1;//错误 p2指向不可修改  const后跟的p2,p2值不可修改(指向不可改),(*p2)前没有const限定,可以修改
	*p2 = 10;//可以通过p2修改p2所指向的变量的值(j2本身可以修改)

	const int* const p31 = &k1;   //指向常量的指针常量 指针值(指针指向)和指针指向的值都不可改变
	const int* const p32 = &k2;   //指向常量的指针常量
}

1.4顶层与底层const

  任意常量对象为顶层const,包括常量指针;指向常量的指针和声明const的引用都为底层const

  顶层const(top-level const)表示指针本身是个常量int* const ptr=&m;

  此时指针不可以发生改变,但是指针所指向的对象值是可以改变的

  底层const(low-level const)表示指针所指的对象是常量const int* ptr=&m;

  此时指针可以发生改变,但是指针所指向的对象值是不可以改变的

  顶层const可以表示任意的对象是常量(指针、引用、int、double都可以)

  于是只有指针和引用等复合类型可以是底层const

  执行对象的拷贝构造时,常量是顶层const还是底层const差别明显

  顶层const并不会有任何影响

进行拷贝操作的时候,仅仅只是从右值(顶层const)拷贝一个值并给自己赋值,虽然右值是一个不可变的量,但是貌似对我自己的拷贝完全没有影响吧

const int m = 10;
int n = m;
int* const ptr2 = &n;
int* ptr3 = ptr2;
int i= 0; 
int *const p1 = &i;//不能改变p1的值,这是一个顶层const
const int ci = 42;//不能改变ci的值,这是一个顶层const
const int *p2 = &ci;//允许改变p2的值 这是一个底层const
const int *const p3 = p2;//靠右的const是顶层const, 靠左的是底层
const const int &r = ci;//用于声明引用的const都是底层const

  当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。 其中,顶层const不受什么影响:

i = ci;//正确:拷贝ci的值,CI是 一个顶层const, 对此操作无影响
p2 = p3;//正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响

  执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。

  另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。非常量可以转换成常量,反之则不行:

int *p = p3;//错误:p3包含底层const的定义,而p没有
p2 = p3;//正确:p2和p3都是底层const ??????????
p2 = &i;//正确:int*能转换成const int* 
int &r = ci;//错误:普通的int&不能绑定到int常量上
const int &r2 = 1;//正确:const int&可以绑定到一个普通int上

  p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p, 因为p指向的是一 个普通的(非常量)整数。 另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const), 仅就这次赋值而言不会有什么影响。

原文链接:https://blog.csdn.net/m0_64860543/article/details/128269607

2.const 函数形参

  我们已经了解了变量中const修饰符的作用,调用函数就会涉及变量参数的问题,那么在形参列表中const形参与非const形参有什么区别呢?

2.1 const 修饰普通形参

同样,先来看看普通变量:

void fun(const int i)
{
	i = 0;
    cout << i << endl;
}

void fun(int i)
{
	i = 0;
    cout << i << endl;
}

int main()
{
    const int i = 1;
    fun(i);
    return 0;
}

  形参的顶层 const 在初始化时会被忽略,所以上面定义的两个函数实际上是一个函数。编译时会出现void fun(int) previously defined here错误。

  • 由于普通变量是拷贝传值,所以const int实参可以传给int形参。

  • 与普通 const 变量一样,第一个 fun 中的形参 i 只可读;第二个function中的 i 则可读可写。

2.2 const 修饰指针形参

  与 const 指针变量一样,指向常量的指针形参指向的值不能修改;本身就是常量的指针形参不能指向其他变量;指向常量的常量指针形参指向的值不能被修改,也不能指向其他变量。

#include<iostream>
using namespace std;
void fun(const int* i)
{
    cout << *i << endl;
}

void fun(int* i)
{
    *i = 0;
    cout << *i << endl;
}

int main()
{
    const int i = 1;
    //调用 fun(const int* i),没有 fun(const int* i),则会编译报错,因为没有匹配形参的函数。
    fun(&i);  
    int j = 1;
    //调用 fun(int* i),没有 fun(int* i),则会调用 fun(const int* i),此时 j 的值不会被改变
    fun(&j);  
    return 0;
}

此外,形参的底层 const 在初始化时不会被忽略,所以上面的两个函数是不同的函数,即重载函数,上面例子编译并不会报错,若果再加上一个void fun(int *const i)就会报错,因为这个函数定义里面 i 是顶层 const。

2.3 const 修饰引用形参

  与const引用一样,const引用不会改变被引用变量的值。

#include<iostream>
using namespace std;
void fun(const int& i)
{
    cout << i << endl;
}

void fun(int& i)
{
    i = 0;
    cout << i << endl;
}

int main()
{
    const int i = 1;
    //调用 fun(const int& i),没有 fun(const int& i),则会编译报错,因为没有匹配形参的函数。
    fun(i);
    int j = 1;
    //调用 fun(int& i),没有 fun(int& i),则会调用 fun(const int& i),此时 j 的值不会被改变
    fun(j);
    return 0;
}

由于 const 引用也是底层 const ,所以上面两个函数是不同的函数,即重载函数,编译并不会报错。

3.类常量成员函数

  面向对象程序设计中,为了体现封装性,通常不允许直接修改类对象的数据成员。若要修改类对象,应调用公有成员函数来完成。为了保证const对象的常量性,编译器须区分试图修改类对象与不修改类对象的函数。例如:

const Screen blankScreen;
blankScreen.display();   // 对象的读操作
blankScreen.set(‘*’);    // 错误:const类对象不允许修改

  C++中的常量对象,以及常量对象的指针或引用都只能调用常量成员函数。

  要声明一个const类型的类成员函数,只需要在成员函数参数列表后加上关键字const,例如:

class Screen 
{
public:
   char get() const;
};

在类外定义const成员函数时,还必须加上const关键字:

char Screen::get() const 
{
   return screen[cursor];
}

若将成员成员函数声明为const,则该函数不允许修改类的数据成员。例如:

class Screen 
{
public:
    int get_cursor() const {return cursor; }
    int set_cursor(int intival) const { cursor = intival; }
};

在上面成员函数的定义中,get_cursor()的定义是合法的,set_cursor()的定义则非法。

值得注意的是,把一个成员函数声明为const可以保证这个成员函数不修改数据成员,但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。例如:

class Name
{
public:
    void setName(const string &s) const;
    char *getName() const;
private:
    char *m_sName;
};
 
void setName(const string &s) const 
{
    m_sName = s.c_str();      // 错误!不能修改m_sName;
 
    for (int i = 0; i < s.size(); ++i) 
        m_sName[i] = s[i];    // 不是错误的
}

const成员函数可以被具有相同参数列表的非const成员函数重载,例如:

class Screen 
{
public:
    char get(int x,int y);
    char get(int x,int y) const;
};

在这种情况下,类对象的常量性决定调用哪个函数。

const Screen cs;
Screen cc2;
char ch = cs.get(0, 0);  // 调用const成员函数
ch = cs2.get(0, 0);     // 调用非const成员函数

const成员函数不能修改类对象数据成员的深层解析:

调用成员函数时,通过一个名为this的隐式参数来访问调用该函数的对象成员。例如:

Name bozai;
bozai.setName("bozai");
bozai.getName("BOZAI");

调用setName时隐式传入 this 形参,通过改变 this->m_sName 的值来改变bozai对象的m_sName。

当调用getName时,同样是隐式传入 this 形参,不过此时的 this 被 const 修饰了,所以不能通过 this 修改对象的成员了。

原文链接:https://blog.csdn.net/weixin_45773137/article/details/126297568

4.constexpr 限定符

4.1常量表达式

常量表达式:指值不会改变并且在编译过程就能得到结果的表达式;字面值、用常量表达式初始化的const对象也是常量表达式。
字面值类型:算术类型、引用和指针都属于字面值类型,自定义类、IO库,string类型则不属于字面值类型,不能被定义成constexpr;

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定, 例如:

const int max_files = 20;//max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式
int staff_size = 27;//staff_size不是常量表达式 
const int sz = get_size();//sz不是常责表达式

  尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

4.2constexpr变量

  在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个const变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此却常常发现初始值并非常量表达式的情况。可以这么说,在此种情况下,对象的定义和使用根本就是两回事儿。

  C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

constexpr int mf = 20;//20是常量表达式
constexpr int limit= mf + l;//mf + 1是常量表达式
constexpr int sz = size();//只有当size是一个constexpr函数时才是一条正确的声明语句

4.3constexpr函数

  尽管不能使用普通函数作为constexpr变量的初始值,但是正如C++ Primer6.5.2节(第214页) 将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。

constexpr函数:指能用于常量表达式的函数,其定义方式和普通函数类型;
定义规则

  • 函数的返回类型及所有形参类型都是字面值类型
  • 函数体中只有一条return 语句
constexpr int func(int n)
{
	return n;
}
int main()
{
	int n=10;
	const int m=10;
	constexpr int i = func(10);//正确,i是一个常量表达式
	constexpr int j = func(n);//错误,n不是字面值
	constexpr int k = func(m+1);//正确,k是一个常量表达式
	return 0;
}
由于func是constexpr函数,编译器能在程序编译时验证func函数返回的是常量表达式,如上面的j,因为n不是常量,所以会在编译期间报错,方便修正
不仅如此,对于constexpr函数,编译器会把constexpr函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数,关于内联函数在这篇C++内联函数博客中有详细介绍
简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值,如果传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了

————————————————
版权声明:本文为CSDN博主「倒地不起的土豆」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37766667/article/details/123915233

字面值类型

  常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为”字面值类型”(literaltype)。

  到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类Sales_item、IO库、string类型则不属于字面值类型,也就不能被定义成 constexpr。其他一些字面值类型将在C++Primer 7.5.6节(第267页)和19.3节(第736页)介绍。
  尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。

  6.1.1节(第184页)将要提到,函数体内定义的变量一般来说并非存放在固定地址中, 因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。同样是在6.1.1节(第185页)中还将提到,允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

4.4const和constexpr区别

  • 对于修饰对象来说,const并未区分出编译期常量和运行期常量,constexpr限定在了编译期常量
  • 在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
const int*p=nullptr;  			//p是一个指向整型常量的指针
constexpr int*q=nullptr;		//q是一个指向整数的常量指针
constexpr const int*p3=nullptr; //p3是一个指向常量的常量指针

  p和q的类型相差甚远, p是一个指向常量的指针,而q是一个本身作为常量的指针, 其中的关键在于constexpr把它所定义的对象置为了顶层const(参见2.4.3节, 第57页)。

3.typedef

3.1什么是typedef

typedef 为C语言的关键字,作用是为一种数据类型定义一个新名字,这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。typedef 本身是一种存储类的关键字,与 auto、extern、static、register 等关键字不能出现在同一个表达式中。

3.2typedef用法

1.对于数据类型使用例如:
typedef  int   NEW_INT;

以上就是给int起了一个新的名字NEW_INT,注意要加分号。当要定义int类型数据时就可以:

NEW_INT num;

此时NEW_INT num 等同于 int num。

2.对于指针的使用例如
typedef  int   *PTRINT;

以上就是给int *起了一个新的名字NEW_INT。可定义int类型指针变量如:

PTRINT x;

此时PTRINT x等同于int *x。

例:


3.对于结构体的使用

在声明结构体时可为结构体和结构体指针起别名,如:

typedef struct NUM
{
     int a;
     int b;
}DATA,*PTRDATA;

此时DATA等同于struct NUM,*PTRDATA等同于struct NUM *。

定义结构体变量及指针可简化为:

   DATA data;           //定义结构体变量
 
   PTRDATA pdata;   //定义结构体指针

举个例子:

#include <stdio.h>
 
typedef struct NUM
{
     int a;
     int b;
}DATA,*PTRDATA;
 
int main()
{
       DATA data;           //定义结构体变量
       PTRDATA pdata;   //定义结构体指针
       pdata=&data;      //结构体指针指向结构体变量      
 
       data.a=100;
       data.b=500;
 
       printf("a=%d\nb=%d\n",data.a,data.b);
       printf("a=%d\nb=%d\n",pdata->a,pdata->b);
       return 0;
}

运行结果:

4.数组指针
int (*ptr)[3];

使用Typedef:

typedef int (*PTR_TO_ARRAY)[3];

例:

#include <stdio.h>
 
typedef int (*PTR_TO_ARRAY)[3];
 
int main()
{
       Int I;
       Int temp[3]={1,2,3};
       PTR_TO_ARRAY ptr_to_array;
       ptr_to_array = &temp;
       for(i+0;i<3;i++)
        {
              printf("%d\n",(*ptr_to_array)[i]);
        }
       return 0;
}
5.指针函数
int (*fun)(void);

使用Typedef:

typedef int (*PTR_TO_FUN)(void);
6.int *(*array[3])(int);
int *(*array[3])(int);

使用Typedef:

typedef int *(*PTR_TO_FUN)(int);

PTR_TO_FUN array[3];

例子:

#include <cstdio>
#include <cstdlib>

void process_array(int* (*arr)[3], int size) 
{
    for (int i = 0; i < size; i++) 
    {
        printf("%d ", (*arr)[i][0]);
    }
}

int main() 
{
    int* (*array)[3];
    array = (int* (*)[3])malloc(sizeof(int*) * 3); // 分配内存空间
    if (array == nullptr)
    {
        printf("内存分配失败\n");
        return 1;
    }

    int data[] = { 1, 2, 3 }; // 包含3个整数的数组
    int* ptr = &data[0]; // 将数组data的第一个元素的地址赋值给指针ptr
    (*array)[0] = ptr; // 将指针ptr的地址赋值给第一个元素的第一个子数组的首元素
    (*array)[1] = ptr + 1; // 将指针ptr加上一个整数的大小后的地址赋值给第一个元素的第二个子数组的首元素
    (*array)[2] = ptr + 2; // 将指针ptr加上两个整数的大小后的地址赋值给第一个元素的第三个子数组的首元素

    int size = sizeof(data) / sizeof(data[0]); // 计算数组data的大小

    process_array(array, size); // 调用process_array函数处理数组数据

    //free((*array)[0]); // 释放第一个元素所占用的内存空间
    //free((*array)[1]); // 释放第二个元素所占用的内存空间
    //free((*array)[2]); // 释放第三个元素所占用的内存空间
    free(array); // 释放主数组所占用的内存空间

    return 0;
}
7.Void (*funA(int,void(*funB)(int)))(int);
void (*funA(int,void(*funB)(int)))(int);
 
void (*funA(参数))(int);

使用Typedef:

typedef void (*PTR_TO_FUN)(void);
 
PTR_TO_FUN funA(int, PTR_TO_FUN);
8.Void (*funA(int,void(*funB)(int)))(int);
void (*funA(int,void(*funB)(int)))(int);

void (*funA(参数))(int);

使用Typedef:

typedef void (*PTR_TO_FUN)(void);
 
PTR_TO_FUN funA(int, PTR_TO_FUN);

4.inline

1.内联函数的引出

  编译过程的最终产品是可执行程序-------由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。当执行到函数调用指令时,程序会将当前指令的内存地址保存到一个特殊的地方,称为调用栈(call stack)或堆栈(stack)。然后,程序会将函数参数复制到堆栈中,以便函数在执行时可以使用这些参数。接下来,程序会跳转到函数定义的位置,即函数起点的内存单元。函数代码在此处执行,期间可能会修改堆栈中的数据或使用其他寄存器。当函数执行结束时,程序会跳回到保存的指令处,即调用栈中保存的地址。此时,程序会恢复到函数调用之前的状态,并且可以使用函数返回的值(返回值放入寄存器)。这个过程被称为函数调用返回序列(function call return sequence),它确保了程序在函数调用和返回之间的状态正确性和一致性。来回跳跃并记录条约位置意味着以前使用函数时,需要一定开销。  

  C++从C中继承的一个重要特征就是效率。假如C++的效率明显低于C的效率,那么就会有很大的一批程序员不去使用C++了。

  在C中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。

但是在C++出现之后,使用预处理宏会出现两个问题:

■第一个在C中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。

■ 第二个问题是C++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类的成员函数。

  为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,C++引入了内联函数(inline function).

  内联函数为了继承宏函数的效率,没有函数调用时的开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。

2.预处理宏的缺陷

  预处理器宏存在问题的关键是我们可能认为预处理器的行为和编译器的行为是一样的。当然也是由于宏函数调用和函数调用在外表看起来是一样的,因为也容易被混淆。但是其中也会有一些微妙的问题出现:

问题一:

#define ADD(x,y) x+y
inline int Add(int x,int y)
{
	return x + y;
}

void test()
{
	int ret1 = ADD(10, 20) * 10; //希望的结果是300
	int ret2 = Add(10, 20) * 10; //希望结果也是300
	cout << "ret1:" << ret1 << endl; //210
	cout << "ret2:" << ret2 << endl; //300
}

问题二:

#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y)
{
	return x < y ? x : y;
}
void test02()
{
	int a = 1;
	int b = 3;
	//cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
	cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}

问题三:

  预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。

3.内联函数

3.1内联函数基本概念

  在C++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。

■在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。

inline void func(int a);

  以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:

inline int func(int a){return a++;}//函数代码行数过多就不太适合做内联函数

注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些是预处理器无法完成的。

内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间

3.2类内部的内联函数

  为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。

class Person
{
public:
	Person(){ cout << "构造函数!" << endl; }
	void PrintPerson(){ cout << "输出Person!" << endl; }
}

  构造函数Person,成员函数PrintPerson在类的内部定义,自动成为内联函数。

3.3内联函数和编译器

  内联函数并不是何时何地都有效,为了理解内联函数何时有效,应该要知道编译器碰到内联函数会怎么处理?

  对于任何类型的函数,编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,当编译器看到内联函数,并且对内联函数体进行分析没有发现错误时,也会将内联函数放入符号表。

  当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不正完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。

  类型检查和类型转换、包括在合适位置放入对象this指针这些都是预处理器不能完成的。

  内联函数不能递归。

  但是C++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:

■不能存在任何形式的循环语句
■不能存在过多的条件判断语句
■函数体不能过于庞大
■不能对函数进行取址操作

  内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。

  内联函数的好处

1.有宏函数的效率,没有宏函数的缺点
2.类的成员函数默认加上inline

    内联函数的缺点

1.占用更多内存,如果程序在10个不同的地方调用同一个内联函数,该程序将包含函数代码的10个副本

  在普通函数前面加上inline是申请成为内联函数

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS 1
//程序清单8.1
//2023年1月8日11:19:40
//inline.cpp--使用内联函数
#include <iostream>
using namespace std;

//内联函数定义
inline double square(double x) { return x * x; }

int main()
{
    double a, b;
    double c = 13.0;

    a = square(5.0);
    b = square(4.5 + 7.5);//可以传递表达式
    cout << "a = " << a << ", b = " << b << "\n";
    cout << "c = " << c;
    cout << ", c squared = " << square(c++) << "\n";
    cout << "Now c = " << c << "\n";

    system("pause");
    return EXIT_SUCCESS;
}

编译器的处理办法
编译器在编译阶段完成对 inline 函数的处理,即对 inline 函数的调用替换为函数的本体。但 inline 关键字对编译器只是一种建议,编译器可以这样去做,也可以不去做。从逻辑上来说,编译器对 inline 函数的处理步骤一般如下:
(1)将 inline 函数体复制到inline函数调用处;
(2)为所用 inline 函数中的局部变量分配内存空间;
(3)将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
(4)如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用GOTO)。

比如如下代码:

// 求 0-9 的平方
inline int inlineFunc(int num) 
{
	if (num > 9 || num < 0) return -1;
	return num * num;
}

int main(int argc, char* argv[]) 
{
	int a = 8;
	int res = inlineFunc(a);
	cout << "res:" << res << endl;
}

inline 之后的 main 函数代码类似于如下形式:

int main(int argc,char* argv[])
{
	int a = 8; 
	{  
	    int _temp_b=8;  
	    int _temp;  
	    if (_temp_q >9||_temp_q<0) _temp = -1;  
	    else _temp =_temp*_temp;  
	    b = _temp;  
	}
}

经过以上处理,可消除所有与调用相关的痕迹以及性能的损失。inline 通过消除调用开销来提升性能。

3.4内联函数优缺点

inline 函数的优点总结如下:

(1)内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。

(2)内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。

例如宏函数和内联函数:

// 宏函数
#define MAX(a,b) ((a)>(b)?(a):(b))

// 内联函数
inline int MAX(int a,int b) 
{
	return a>b?a:b;
}

使用宏函数时,其书写语法也较为苛刻,如果对宏函数出现如下错误的调用,MAX(a,"Hello"); 宏函数会错误地比较int和字符串,没有参数类型检查,但是使用内联函数的时候,会出现类型不匹配的编译错误。

(3)在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。

(4)内联函数在运行时可调试,而宏定义不可以。

inline 函数的缺点总结如下:

(1)代码膨胀。

inline 函数带来的运行效率是典型的以空间换时间的做法。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。

(2)inline 函数无法随着函数库升级而升级。
如果f是函数库中的一个inline函数,使用它的用户会将f函数实体编译到他们的程序中。一旦函数库实现者改变f,所有用到f的程序都必须重新编译。如果f是non-inline的,用户程序只需重新连接即可。如果函数库采用的是动态连接,那这一升级的f函数可以不知不觉的被程序使用。

(3)是否内联,程序员不可控。

inline 函数只是对编译器的建议,是否对函数内联,决定权在于编译器。编译器认为调用某函数的开销相对该函数本身的开销而言微不足道或者不足以为之承担代码膨胀的后果则没必要内联该函数,若函数出现递归,有些编译器则不支持将其内联。

3.5注意事项

了解了内联函数的优缺点,在使用内联函数时,我们也要注意以下几个事项和建议。

(1)使用函数指针调用内联函数将会导致内联失败。

也就是说,如果使用函数指针来调用内联函数,那么就需要获取inline函数的地址。如果要取得一个inline函数的地址,编译器就必须为此函数产生一个函数实体,那么就内联失败。

(2)如果函数体代码过长或者有多重循环语句,if 或 witch 分支语句或递归时,不宜用内联。

(3)类的 constructors、destructors 和虚函数往往不是 inline 函数的最佳选择。

类的构造函数(constructors)可能需要调用父类的构造函数,析构函数同样可能需要调用父类的析构函数,二者背后隐藏着大量的代码,不适合作为inline函数。虚函数(destructors)往往是运行时确定的,而inline是在编译时进行的,所以内联虚函数往往无效。如果直接用类的对象来使用虚函数,那么对有的编译器而言,也可起到优化作用。

(4)至于内联函数是定义在头文件还是源文件的建议。

内联展开是在编译时进行的,只有链接的时候源文件之间才有关系。所以内联要想跨源文件必须把实现写在头文件里。如果一个 inline 函数会在多个源文件中被用到,那么必须把它定义在头文件中。

// base.h
class Base { protected:void fun(); };

// base.cpp
#include base.h
inline void Base::fun() {}

// derived.h
#include base.h
class Derived : public Base { public:void g(); };

// derived.cpp
void Derived::g() { fun(); } // VC2010: error LNK2019: unresolved external symbol

上面这种错误,就是因为内联函数 fun() 定义在编译单元 base.cpp 中,那么其他编译单元中调用fun()的地方将无法解析该符号,因为在编译单元 base.cpp 生成目标文件 base.obj 后,内联函数fun()已经被替换掉,编译器不会为 fun() 生成函数实体,链接器自然无法解析。所以如果一个 inline 函数会在多个源文件中被用到,那么必须把它定义在头文件中。

这里有个问题,当在头文件中定义内联函数,那么被多个源文件包含时,如果编译器因为 inline 函数不适合被内联时,拒绝将inline函数进行内联处理,那么多个源文件在编译生成目标文件后都将各自保留一份inline函数的实体,这个时候程序在链接阶段会出现重定义错误吗?答案是不会,原因是,链接器在链接的过程中,会删除多余的 inline 函数实体,只保留一份,所以不会报重定义错误,因此我们不需要使用 static 关键字去多余地修饰 inline 函数,即不必像下面这样。

// test.h
static inline int max(int a,int b)
{
	return a>b?a:b;
}

(5)能否强制编译器进行内联操作?

也有人可能会觉得能否强制编译器进行函数内联,而不是建议编译器进行内联呢?很不幸的是目前还不能强制编译器进行函数内联,如果使用的是 MS VC++, 注意 __forceinline 如同 inine 一样,也是一个用词不当的表现,它只是对编译器的建议比inline更加强烈,并不能强制编译器进行 inline 操作。

(6)如何查看函数是否被内联处理了?

在 VS2017 中查看预处理后的.i文件,发现inline函数的内联处理不是在预处理阶段,而是在编译阶段。将源文件编译为汇编代码,或者将可执行文件反汇编生成汇编代码,在汇编代码中查看inline函数被调用处是否出现汇编的call指令,如果没有则说明inline函数在被调用处进行了函数体的替换操作,即内联处理。具体可以参考内联函数到底有没有被嵌入到调用处呢?。
(7)C++ 类成员函数定义在类体内为什么不会报重定义错误?
类成员函数定义在类体内,并随着类的定义放在头文件中,当被不同的源文件包含,那么每个源文件都应该包含了类成员函数的实体,为何在链接的过程中不会报函数的重定义错误呢?

原因是在类里定义时,这种函数会被编译器编译成内联函数,在类外定义的函数则不会。内联函数的好处是加快程序的运行速度,缺点是会增加程序的尺寸。比较推荐的写法是把一个经常要用的而且实现起来比较简单的小型函数放到类里去定义,大型函数最好还是放到类外定义。

可能存在疑问,类体内的成员函数被编译器内联处理,但并不是所有的成员函数都会被内联处理,比如包含递归的成员函数。但是实际测试,将包含递归的成员函数定义在类体内,被不同的源文件包含并不会报重定义错误,为什么会这样呢?请保持着疑问与好奇心,请继续往下看。

如果编译器发现被定义在类体内的成员函数无法被内联处理,那么在程序的链接过程中也不会出现函数重定义的错误。其原因是什么呢?其实很简单,类体内定义的成员函数即使不被内联处理,在链接时,链接器会对重复的成员函数实体进行冗余优化,只保留一份函数实体,也就不会出现函数重定义的错误了。
除了 inline 函数,C++编译器在很多时候都会产生重复的代码,比如模板(Templates)、虚函数表(Virtual Function Table)、类的默认成员函数(构造函数、析构函数和赋值运算符)等。以函数模板为例,在多个源文件中生成相同的实例,链接时不会出现函数重定义的错误,实际上是一个道理,因为链接器会对重复代码进行删除,只保留一份函数实体。

3.6小结

可以将内联理解为 C++ 中对于函数专有的宏,对于 C 的函数宏的一种改进。对于常量宏,C++ 提供 const 替代;而对于函数宏,C++ 提供的方案则是 inline。C++ 的内联机制,既具备宏代码的效率,又增加了安全性,还可以自由操作类的数据成员,算是一个比较完美的解决方案。

一、const与#define的区别:

  1. const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
  2. define只在预处理阶段起作用,简单的文本替换,而const在编译、链接过程中起作用;
  3. define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
  4. define预处理后,占用代码段空间,const占用数据段空间;
  5. const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
  6. define独特功能,比如可以用来防止文件重复引用。

二、#define和别名typedef的区别

  1. 执行时间不同,typedef在编译阶段有效,typedef有类型检查的功能;#define是宏定义,发生在预处理阶段,不进行类型检查;
  2. 功能差异,typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
  3. 作用域不同,#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而typedef有自己的作用域。

三、 define与inline的区别

  1. #define是关键字,inline是函数;
  2. 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
  3. inline函数有类型检查,相比宏定义比较安全;

————————————————
版权声明:本文为CSDN博主「恋喵大鲤鱼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/K346K346/article/details/52065524

参考资料

参考资料来源于黑马程序员等

posted @ 2023-07-11 15:24  CodeMagicianT  阅读(83)  评论(0编辑  收藏  举报