问一下Cpp中的时间函数?

Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。

以 Linux 下的C语言开发为例来具体说明一下,多文件编译生成一个文件,编译的命令如下所示:

下面是一些常用的隐含规则:

  • 汇编和需要预处理的汇编程序

  • Lex C 程序时的隐含规则

上面的编译顺序都是一些常用的编程语言执行隐含规则的顺序,我们在 Makefile 中指定规则时,可以参考这样的列表。当需要编译源文件的时候,考虑是不是需要使用隐含规则。如果不需要,就要把相应的规则和命令全部书写上去。

内嵌隐含规则的命令中,所使用的变量都是预定义的。我们将这些变量称为“隐含变量”。这些变量允许修改:可以通过命令行参数传递或者是设置系统环境变量的方式都可以对它进行重新定义。无论使用哪种方式,只要 make 在运行的,这些变量的定义有效。Makefile 的隐含规则都会使用到这些变量。

比如我们编译 .c 文件在我们的 Makefile 中就是隐含的规则,默认使用到的编译命令时cc,执行的命令时cc -c我们可以对用上面的任何一种方式将CC定义为ncc。这样我们就编译 .c 文件的时候就可以用ncc进行编译。

隐含规则中使用的变量可以分成两类:

1.代表一个程序的名字。例如:“CC”代表了编译器的这个可执行程序。

2.代表执行这个程序使用的参数.例如:变量“CFLAGS”。多个参数之间使用空格隔开。

下面我们来列举一下代表命令的变量,默认都是小写。

  • AR:函数库打包程序,科创价静态库 .a 文档。

  • AS:应用于汇编程序。

  • CO:从 RCS 中提取文件的程序。

  • CPP:C程序的预处理器。

  • FC:编译器和与处理函数 Fortran 源文件的编译器。

  • GET:从CSSC 中提取文件程序。

  • YACC:Yacc 文法分析器(针对于C语言)

日常使用 Makefile 编译文件时,可能会遇到需要分条件执行的情况,比如在一个工程文件中,可编译的源文件很多,但是它们的类型是不相同的,所以编译文件使用的编译器也是不同的。手动编译去操作文件显然是不可行的(每个文件编译时需要注意的事项很多),所以 make 为我们提供了条件判断来解决这样的问题。

需要解决的问题:要根据判断,分条件执行语句。

条件语句的作用:条件语句可以根据一个变量的值来控制 make 执行或者时忽略 Makefile 的特定部分,条件语句可以是两个不同的变量或者是常量和变量之间的比较。

条件语句使用优点:Makefile 中使用条件控制可以做到处理的灵活性和高效性。注意:条件语句只能用于控制 make 实际执行的 Makefile 文件部分,不能控制规则的 shell 命令执行的过程。

下面是条件判断中使用到的一些关键字:

条件判断的使用方式如下:

条件语句中使用到三个关键字“ifeq”、“else”、“endif”。其中:“ifeq”表示条件语句的开始,并指定一个比较条件(相等)。括号和关键字之间要使用空格分隔,两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。“ifeq”,后面的是条件满足的时候执行的,条件不满足忽略;“else”表示当条件不满足的时候执行的部分,不是所有的条件语句都要执行此部分;“endif”是判断语句结束标志,Makefile 中条件判断的结束都要有。

其实 "ifneq" 和 "ifeq" 的使用方法是完全相同的,只不过是满足条件后执行的语句正好相反。

上面的例子可以换一种更加简介的方式来写:

它的主要功能是判断变量的值是不是为空,实例:

通过两个实例对比说明:通过打印 "yes" 或 "no" 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 "yes" ,实例 2打印的结果是 "no" 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。

注意:在 make 读取 Makefile 文件时计算表达式的值,并根据表达式的值决定判断语句中的哪一个部分作为此 Makefile 所要执行的内容。因此在条件表达式中不能使用自动化变量,自动化变量在规则命令执行时才有效,更不能将一个完整的条件判断语句分卸在两个不同的 Makefile 的文件中。在一个 Makefile 中使用指示符 "include" 包含另一个 Makefile

伪目标可以这样来理解,它并不会创建目标文件,只是想去执行这个目标下面的命令。伪目标的存在可以帮助我们找到命令并执行。

使用伪目标有两点原因:

  • 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。

  • 提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。

我们先来看一下第一种情况的使用。如果需要书写这样一个规则,规则所定义的命令不是去创建文件,而是通过 make 命令明确指定它来执行一些特定的命令。实例:

规则中 rm 命令不是创建文件 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 总会被执行 ,这也是我们期望的结果。

如果当前目录下存在文件名为 clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。为了解决这个问题,删除 clean 文件或者是在 Makefile 中将目标 clean 声明为伪目标。将一个目标声明称伪目标的方法是将它作为特殊的目标.PHONY的依赖,如下:

这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。

在书写伪目标的时候,需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标 "clean" 的完整书写格式如下:

伪目标的另一种使用的场合是在 make 的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要 make 的子目录。对多个目录进行 make 的实现,可以在一个规则的命令行中使用 shell 循环来完成。如下:

代码表达的意思是当前目录下存在三个子文件目录,每个子目录文件都有相对应的 Makefile 文件,代码中实现的部分是用当前目录下的 Makefile 控制其它子模块中的 Makefile 的运行,但是这种实现方法存在以下几个问题:

  • 当子目录执行 make 出现错误时,make 不会退出。就是说,在对某个目录执行 make 失败以后,会继续对其他的目录进行 make。在最终执行失败的情况下,我们很难根据错误提示定位出具体实在那个目录下执行 make 发生的错误。这样给问题定位造成很大的困难。为了解决问题可以在命令部分加入错误检测,在命令执行的错误后主动退出。不幸的是如果在执行 make 时使用了 "-k"

  • 另外一个问题就是使用这种 shell 循环方式时,没有用到 make 对目录的并行处理功能由于规则的命令时一条完整的 shell 命令,不能被并行处理。

有了伪目标之后,我们可以用它来克服以上方式所存在的两个问题,代码展示如下:

上面的实例中有一个没有命令行的规则“foo:baz”,这个规则是用来规定三个子目录的编译顺序。因为在规则中 "baz" 的子目录被当作成了 "foo" 的依赖文件,所以 "baz" 要比 "foo" 子目录更先执行,最后执行 "bar" 子目录的编译。

一般情况下,一个伪目标不作为另外一个目标的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则伪目标所定义的命令都会被执行(因为它作为规则的依赖,重建规则目标时需要首先重建规则的所有依赖文件)。当一个伪目标没有任何目标(此目标是一个可被创建或者是已存在的文件)的依赖时,我们只能通过 make 的命令来明确的指定它的终极目标,执行它所在规则所定义的命令。例如 make

如果在一个文件里想要同时生成多个可执行文件,我们可以借助伪目标来实现。使用方式如下:

我们在当前目录下创建了三个源文件,目的是把这三个源文件编译成为三个可执行文件。将重建的规则放到 Makefile 中,约定使用 "all" 的伪目标来作为最终目标,它的依赖文件就是要生成的可执行文件。这样的话只需要一个 make 命令,就会同时生成三个可执行文件。

之所以这样写,是因为伪目标的特性,它总会被执行,所以它依赖的三个文件的目标就不如 "all" 这个目标新,所以,其他的三个目标的规则总是被执行,这也就达到了我们一口气生成多个目标的目的。我们也可以实现单独的编译这三个中的任意一个源文件(我们想去重建 test1,我们可以执行命令make test1 来实现 )。

Makefile常用字符串处理函数

函数的调用和变量的调用很像。引用变量的格式为$(变量名),函数调用的格式如下:

其中,function 是函数名,arguments 是函数的参数,参数之间要用逗号分隔开。而参数和函数名之间使用空格分开。调用函数的时候要使用字符“$”,后面可以跟小括号也可以使用花括号。这个其实我们并不陌生,我们之前使用过许多的函数,比如说展开通配符的函数 wildcard,以及字符串替换的函数 patsubst ,Makefile 中函数并不是很多。

今天主要讲的是字符串处理函数,这些都是我们经常使用到的函数,下面是对函数详细的介绍。

1. 模式字符串替换函数,函数使用格式如下:

函数说明:函数功能是查找 text 中的单词是否符合模式 pattern,如果匹配的话,则用 replacement 替换。返回值为替换后的新字符串。实例:

执行 make 命令,我们可以得到的值是 "1.o 2.o 3.o",这些都是替换后的值。

2. 字符串替换函数,函数使用格式如下:

函数说明:函数的功能是把字符串中的 form 替换成 to,返回值为替换后的新字符串。实例:

3. 去空格函数,函数使用格式如下:

函数说明:函数的功能是去掉字符串的开头和结尾的字符串,并且将其中的多个连续的空格合并成为一个空格。返回值为去掉空格后的字符串。实例:

执行完 make 之后,结果是“a b c”。这个只是除去开头和结尾的空格字符,并且将字符串中的空格合并成为一个空格。

4. 查找字符串函数,函数使用格式如下:

函数说明:函数的功能是查找 in 中的 find ,如果我们查找的目标字符串存在。返回值为目标字符串,如果不存在就返回空。实例:

执行 make 命令,得到的返回的结果就是 "a"。

5. 过滤函数,函数使用格式如下:

函数说明:函数的功能是过滤出 text 中符合模式 pattern 的字符串,可以有多个 pattern 。返回值为过滤后的字符串。实例:

执行 make 命令,我们得到的值是“1.c 2.o”。

6. 反过滤函数,函数使用格式如下:

函数说明:函数的功能是功能和 filter 函数正好相反,但是用法相同。去除符合模式 pattern 的字符串,保留符合的字符串。返回值是保留的字符串。实例:

执行 make 命令,打印的结果是“3.s”。

7. 排序函数,函数使用格式如下:

函数说明:函数的功能是将 <list>中的单词排序(升序)。返回值为排列后的字符串。实例:

执行 make 命令,我们得到的值是“bar foo lost”。注意:sort会去除重复的字符串。

8. 取单词函数,函数使用格式如下:

函数说明:函数的功能是取出函数<text>中的第n个单词。返回值为我们取出的第 n 个单词。实例:

执行 make 命令,我们得到的值是“2.c”。

Makefile常用文件名操作函数

我们在编写 Makefile 的时候,很多情况下需要对文件名进行操作。例如获取文件的路径,去除文件的路径,取出文件前缀或后缀等等。当遇到这样的问题的时手动修改是不太可能的,因为文件可能会很多,而且 Makefile 中操作文件名可能不止一次。所以 Makefile 给我们提供了相应的函数去实现文件名的操作。

注意:下面的每个函数的参数字符串都会被当作或是一个系列的文件名来看待。

1. 取目录函数,函数使用格式如下:

函数说明:函数的功能是从文件名序列 names 中取出目录部分,如果没有 names 中没有 "/" ,取出的值为 "./" 。返回值为目录部分,指的是最后一个反斜杠之前的部分。如果没有反斜杠将返回“./”。实例:

2. 取文件函数,函数使用格式如下:

函数说明:函数的功能是从文件名序列 names 中取出非目录的部分。非目录的部分是最后一个反斜杠之后的部分。返回值为文件非目录的部分。实例:

3. 取后缀名函数,函数使用格式如下:

函数说明:函数的功能是从文件名序列中 names 中取出各个文件的后缀名。返回值为文件名序列 names 中的后缀序列,如果文件没有后缀名,则返回空字符串。实例:

执行 make 命令,我们得到的值是“.c ”。文件 "hacks" 没有后缀名,所以返回的是空值。

4. 取前缀函数,函数使用格式如下:

函数说明:函数的功能是从文件名序列 names 中取出各个文件名的前缀部分。返回值为被取出来的文件的前缀名,如果文件没有前缀名则返回空的字符串。实例:

执行 make 命令,我们可以得到值是“src/foo hacks”。获取的是文件的前缀名,包含文件路径的部分。

5. 添加后缀名函数,函数使用格式如下:

函数说明:函数的功能是把后缀 suffix 加到 names 中的每个单词后面。返回值为添加上后缀的文件名序列。实例:

执行 make 后我们可以得到“sec/foo.c.c hack.c”。我们可以看到如果文件名存在后缀名,依然会加上。

6. 添加前缀名函数,函数使用格式如下:

函数说明:函数的功能是把前缀 prefix 加到 names 中的每个单词的前面。返回值为添加上前缀的文件名序列。实例:

执行 make 命令,我们可以得到值是 "src/foo.c src/hacks" 。我们可以使用这个函数给我们的文件添加路径。

7. 链接函数,函数使用格式如下:

函数说明:函数功能是把 list2 中的单词对应的拼接到 list1 的后面。如果 list1 的单词要比 list2的多,那么,list1 中多出来的单词将保持原样,如果 list1 中的单词要比 list2 中的单词少,那么 list2 中多出来的单词将保持原样。返回值为拼接好的字符串。实例:

8. 获取匹配模式文件名函数,命令使用格式如下:

函数说明:函数的功能是列出当前目录下所有符合模式的 PATTERN 格式的文件名。返回值为空格分隔并且存在当前目录下的所有符合模式 PATTERN 的文件名。实例:

执行 make 命令,可以得到当前函数下所有的 ".c " 和 ".h" 结尾的文件。这个函数通常跟的通配符 "*" 连用,使用在依赖规则的描述的时候被展开(在这里我们的例子如果没有 wildcard 函数,我们的运行结果也是这样,"echo" 属于 shell 命令,在使用通配符的时通配符自动展开,我们这里只是相要说明一下这个函数在使用时,如果通过引用变量出现在规则中要被使用)。

Makefile 中的其他的函数。以下是这些函数的详细说明。

函数的功能是:把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>的返所返回的每个字符串会以空格分割,最后当整个循环结束的时候,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是

可见,if 函数可以包含else部分,或者是不包含,即if函数的参数可以是两个,也可以是三个。condition参数是 if 表达式,如果其返回的是非空的字符串,那么这个表达式就相当于返回真,于是,then-part就会被计算,否则else-part会被计算。

而if函数的返回值是:如果condition为真(非空字符串),那么then-part会是整个函数的返回值。如果condition为假(空字符串),那么else-part将会是这个函数的返回值。此时如果else-part没有被定义,那么整个函数返回空字串符。所以,then-partelse-part只会有一个被计算。

执行 make 命令我们可以得到函数的值是 foo.c,如果变量 OBJ 的值为空的话,我们得到的 OBJ 的值就是main.c

call 函数是唯一一个可以用来创建新的参数化的函数。我们可以用来写一个非常复杂的表达式,这个表达式中,我们可以定义很多的参数,然后你可以用 call 函数来向这个表达式传递参数。

那么,foo 的值就是“a b”。当然,参数的次序可以是自定义的,不一定是顺序的,

此时的 foo 的值就是“b a”。

origin 函数不像其他的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的。注意: variable 是变量的名字,不应该是引用,所以最好不要在 variable 中使用“$”字符。origin 函数会员其返回值来告诉你这个变量的“出生情况”。

下面是origin函数返回值:

这些信息对于我们编写 Makefile 是非常有用的,例如假设我们有一个 Makefile ,其包含了一个定义文件Make.def,在Make.def中定义了一个变量bletch,而我们的环境变量中也有一个环境变量bletch,我们想去判断一下这个变量是不是环境变量,如果是我们就把它重定义了。如果是非环境变量,那么我们就不重新定义它。于是,我们在 Makefile

当然,使用override关键字不就可以重新定义环境中的变量了吗,为什么需要使用这样的步骤?是的,我们用override是可以达到这样的效果的,可是override会把从命令行定义的变量也覆盖了,而我们只想重新定义环境传来的,而不是重新定义命令行传来的。

令是由 shell 命令行组成,他们是一条一条执行的。多个命令之间要使用分号隔开,Makefile 中的任何命令都要以tab键开始。多个命令行之间可以有空行和注释行,在执行规则时空行会被自动忽略。

通常系统中可能存在不同的 shell 。但是 make 处理 Makefile 过程时,如果没有明确的指定,那么对所有规则中的命令行的解析使用bin/sh来完成。执行过程中使用的 shell 决定了规则中的命令的语法和处理机制。当使用默认的bin/sh时,命令中出现的字符“#”到行末的内容被认为是注释。当然了“#”可以不在此行的行首,此时“#”之前的内容不会被作为注释处理。

通常 make 在执行命令行之前会把要是执行的命令行输出到标准输出设备。我们称之为 "回显",就好像我们在 shell 环境下输入命令执行时一样。如果规则的命令行以字符“@”开始,则 make 在执行的时候就不会显示这个将要被执行的命令。典型的用法是在使用echo命令输出一些信息时。

我们在执行 make 时添加上一些参数,可以控制命令行是否输出。当使用 make 的时候机加上参数-n或者是--just-print ,执行时只显示所要执行的命令,但不会真正的执行这个命令。只有在这种情况下 make 才会打印出所有的 make 需要执行的命令,其中包括了使用的“@”字符开始的命令。这个选项对于我们调试 Makefile 非常的有用,使用这个选项就可以按执行顺序打印出 Makefile 中所需要执行的所有命令。而 make 参数-s或者是--slient则是禁止所有的执行命令的显示。就好像所有的命令行都使用“@”开始一样。

当规则中的目标需要被重建的时候,此规则所定义的命令将会被执行,如果是多行的命令,那么每一行命令将是在一个独立的子 shell 进程中被执行。因此,多命令行之间的执行命令时是相互独立的,相互之间不存在依赖。

在 Makefile 中书写在同一行中的多个命令属于一个完整的 shell 命令行,书写在独立行的一条命令是一个独立的 shell 命令行。因此:在一个规则的命令中命令行 “cd”改变目录不会对其后面的命令的执行产生影响。就是说之后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。如果达到这个目的,就不能把“cd”和其后面的命令放在两行来书写。而应该把这两个命令放在一行上用分号隔开。这样才是一个完整的 shell 命令行。

如果想把一个完整的shell命令行书写在多行上,需要使用反斜杠 ()来对处于多行的命令进行连接,表示他们是一个完整的shell命令行。例如上例我们也可以这样书写:

make 对所有规则的命令的解析使用环境变量“SHELL”所指定的那个程序。在 GNU make 中,默认的程序时 “/bin/sh”。不像其他的绝大多数变量,他们的只可以直接从同名的系统环境变量那里获得。make 的环境变量 “SHELL”没有使用环境变量的定义。因为系统环境变量“SHELL”指定的那个程序被用来作为用户和系统交互的接口程序,他对于不存在直接交互过程的 make 显然不合适。在 make 环境变量中“SHELL”会被重新赋值;他作为一个变量我们也可以在 Makefile 中明确的给它赋值,变量“SHELL“的默认值时“/bin/sh”。

GNU make 支持同时执行多条命令。通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令结束之后才能够开始执行。不过可以通过 make 命令行选项 "-j" 或者 "--jobs" 来告诉 make 在同一时刻可以允许多条命令同时执行。

如果选项 "-j" 之后存在一个整数,其含义是告诉 make 在同一时刻可以允许同时执行的命令行的数目。这个数字被称为job slots。当 "-j" 选项中没有出现数字的时候,那么同一时间执行的命令数目没有要求。使用默认的job solts,值为1,表示make将串行的执行规则的命令(同一时刻只能由一条命令被执行)。

并行执行命令所带来的问题是显而易见的:

  • 多个同时执行的命令的输出信息将同时被输出到终端。当出现错误时很难根据一大堆凌乱的信息来区分那条命令执行错误。

  • 在同一时刻可能会存在多个命令执行的进程同时读取到标准输入,但是对于白哦准输入设备来说,在同一时刻只能存在一个进程访问它。就是说在某个时间点,make只能保证此刻正在执行的进程中的一个进程读取标准输入流。而其他的进程键的标准输入流将设置为无效。因此在此一时刻多个执行命令的进程中只有一个进程获得标准输入,而其他的需要读取标准输入流的进程由于输入流无效而导致致命的错误。

包含其他文件使用的关键字是 "include",和 C 语言包含头文件的方式相同。

filenames 是 shell 支持的文件名(可以使用通配符表示的文件)。注意:"include" 关键字所在的行首可以包含一个或者是多个的空格(读取的时候空格会被自动的忽略),但是不能使用 Tab 开始,否则会把 "include" 当作式命令来处理。包含的多个文件之间要使用空格分隔开。使用 "include" 包含进来的 Makefile 文件中,如果存在函数或者是变量的引用,它们会在包含的 Makefile 中展开。

include 通常使用在以下的场合:

  • 在一个工程文件中,每一个模块都有一个独立的 Makefile 来描述它的重建规则。它们需要定义一组通用的变量定义或者是模式规则。通用的做法是将这些共同使用的变量或者模式规则定义在一个文件中,需要的时候用 "include" 包含这个文件。

  • 当根据源文件自动产生依赖文件时,我们可以将自动产生的依赖关系保存在另一个文件中。然后在 Makefile 中包含这个文件。

注意:如果使用 "include" 包含文件的时候,指定的文件不是文件的绝对路径或者是为当前文件下没有这个文件,make 会根据文件名会在以下几个路径中去找,首先我们在执行 make 命令的时候可以加入选项 "-I" 或 "--include-dir"

如果在上面的路径没有找到 "include" 指定的文件,make 将会提示一个文件没有找到的警示提示,但是不会退出,而是继续执行 Makefile 的后续的内容。当完成读取整个 Makefile 后,make 将试图使用规则来创建通过 "include" 指定但不存在的文件。当不能创建的时候,文件将会保存退出。

使用方法和 "include" 的使用方法相同。

这两种方式之间的区别:

  • 使用 "include <filenames>" ,make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出。

  • 使用 "-include <filenames>",当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出。

我们都知道在一个大的工程文件中,不同的文件按照功能被划分到不同的模块中,也就说很多的源文件被放置在了不同的目录下。每个模块可能都会有自己的编译顺序和规则,如果在一个 Makefile 文件中描述所有模块的编译规则,就会很乱,执行时也会不方便,所以就需要在不同的模块中分别对它们的规则进行描述,也就是每一个模块都编写一个 Makefile 文件,这样不仅方便管理,而且可以迅速发现模块中的问题。这样我们只需要控制其他模块中的 Makefile 就可以实现总体的控制,这就是 make 的嵌套执行。

如何来使用呢?举例说明如下:

这个例子可以这样来理解,在当前目录下有一个目录文件 subdir 和一个 Makefile 文件,子目录 subdir 文件下还有一个 Makefile 文件,这个文件是用来描述这个子目录文件的编译规则。使用时只需要在最外层的目录中执行 make 命令,当命令执行到上述的规则时,程序会进入到子目录中执行 make。这就是嵌套执行 make,我们把最外层的 Makefile 称为是总控

上述的规则也可以换成另外一种写法:

在 make 的嵌套执行中,我们需要了解一个变量 "CURDIR",此变量代表 make 的工作目录。当使用 make 的选项 "-C" 的时候,命令就会进入指定的目录中,然后此变量就会被重新赋值。总之,如果在 Makefile 中没有对此变量进行显式的赋值操作,那么它就表示 make 的工作目录。我们也可以在 Makefile 中为这个变量赋一个新的值,当然重新赋值后这个变量将不再代表 make 的工作目录。

使用 make 嵌套执行的时候,变量是否传递也是我们需要注意的。如果需要变量的传递,那么可以这样来使用:export <variable>

<variable>是变量的名字,不需要使用 "$" 这个字符。如果所有的变量都需要传递,那么只需要使用 "export" 就可以,不需要添加变量的名字。

make 命令传递的参数,并且会传递到下层的 Makefile 中,这是一个系统级别的环境变量。

案例:通过一个大的项目工程来详细的分析一下如何嵌套执行 make。

假设有一个 MP3 player 的应用程序,它可以被划分为若干个组件:用户界面(ui)、编解码器(codec)以及数据管理库(db)。它们分别可以用三个程序库来表示:libui.a、libcodec.a 和 libdb.a。将这些组件紧凑的放到一起就可以组成这个应用程序。具体的文件结构展示为(我们展示的只是目录文件,没有展示详细的源文件):

├──include //编译的时候需要链接的库文件

├──lib //源文件所在的目录,子目录文件中包含Makefile文件

│ ├──codec //编解码器所在的源文件的目录

│ ├──db //数据库源文件所在的目录

│ ├──ui //用户界面源文件所在目录

└──doc //这个工程编译说明

我们可以看到最外层有一个 Makefile 文件,这就是我们的 "总控Makefile" 文件,我们使用这个 Makefile 调用项目中各个子目录的 Makefile 文件的运行。假设只有我们的 lib 目录下和 app 目录下的各个子目录含有 Makefile 文件。那我们总控的 Makefile 的文件可以这样来写:

我们可以看到在 "总控 Makefile" 中,一个规则在工作目标上列出了所有的子目录,它对每一个子目录的 Makefile 调用的代码是:(libraries) :

在 Makefile 文件中,MAKE 变量应该总是用来调用 make 程序。make 程序一看到 MAKE 变量就会把它设成 make 的实际路径,所以递归调用中的每次调用都会使用同一个执行文件。此外,当命令 --touch(-t)、--just-print(-n) 和 --question(-q) 被使用时,包含 MAKE 变量的每一行都会受到特别的处理。

由于这些“工作目标目录”被设成 .PHONY 的依赖文件,所以即使工作目标已经更新,此规则仍旧会进行更新动作。使 --directory(-C) 选项的目的是要让 make 在读取 Makefile 之前先切换到相应的 "工作目录" 。

当 make 在建立依存图的时候找不到程序库与 app/player 工作目标之间的依存关系时,这意味着建立任何程序库之前,make 将会先执行 app/player 目录中的 Makefile。显然这将会导致失败的结果,因为应用程序的链接需要程序库。为解决这个问题,我们会提供额外的依存信息:

我们在此处做了如下的描述:运行 app/player 目录中的 Makefile 之前必须先运行程序库子目录中的 Makefile。此外,编译 lib/ui 目录中的程序代码之前必须先编译 lib/db 和lib/codec 目录中的程序库。这么做可以确保任何自动产生的程序代码,在 lib/ui 目录中的程序代码被编译之前就已经产生出来了。

更新必要条件的时候,会引发微妙的次序问题。如同所有的依存关系,更新的次序取决于依存图的分析结果,但是当工作目标的必要条件(依赖文件)出现在同一行时,GNU make 将会从左至右的次序进行更新。例如:

如果不存在其他的依存关系,这6个必要条件的更新动作可以是任何次序,不过GNU make将会以从左向右的次序来更新出现在同一行的必要条件,这会产生如下的更新次序:"a b c d e f" 或 "d e f a b c"。注意:不要因为之前这么做更新的次序是对的,就以为每次这么做都是对的,而忘了提供完整的依存信息。

最后,依存分析可能会产生不同的次序而引发一些问题。所以,如果有一组工作目标需要以特定的次序进行更新时,就必须提供适当的必要条件来实现正确的次序。

当我们在最外层执行 make 的时候我们会看到l输出的信息:

变量的值加上方括号之后被一起输出。在这个简单的例子里,每个组件的 Makefile 只会输出组件正在更新的信息,而不会真正的更新组件。

我们通过这个例子应该可以了解,在 make 的嵌套执行执行的时候的调用子目录的方式,还有子目录再去执行 make 时候的顺序。这是一个很典型的例子,我们的每一个工程文件都可以用上面的结构展示出来,我们只要懂得每一个子目录在被调用时候的顺序,我们就可以很轻松的编写 "总控Makefile" 。

make命令参数和选项大汇总

我们在在执行 make 命令时,有的时候需要加上一下参数选项来保证我们的程序的执行,其实之前已经遇到过 make 在执行命令的时候需要添加上参数选项,比如只打印命令但不执行使用的参数是 "-n" ,还有只执命令不打印命令的参数选项是 "-s",包含其它文件的路径参数选项是 "-include"等等。

我们现在列举一下 make 可以使用的参数选项,以及它们的功能是什么。

模式规则中的目标。规则中的目标形式是多种多样的,它可以是一个或多个的文件、可以是一个伪目标,这是我们之前讲到过的,也是经常使用的。其实规则目标还可以是其他的类型,下面是对这些类型的详细的说明。

如果一个目标中没有命令或者是依赖,并且它的目标不是一个存在的文件名,在执行此规则时,目标总会被认为是最新的。就是说:这个规则一旦被执行,make 就认为它的目标已经被更新过。这样的目标在作为一个规则的依赖时,因为依赖总被认为更新过,因此作为依赖在的规则中定义的命令总会被执行。看一个例子:

这个例子中,目标 "FORCE" 符合上边的条件。它作为目标 "clean" 的依赖,在执行 make 的时候,总被认为更新过。因此 "clean" 所在的规则而在被执行其所定义的那个命令总会被执行。这样的一个目标通常我们将其命名为 "FORCE"。

例子中使用 "FORCE" 目标的效果和将 "clean" 声明为伪目标的效果相同。

空目标文件是伪目标的一个变种,此目标所在的规则执行的目的和伪目标相同——通过 make 命令行指定将其作为终极目标来执行此规则所定义的命令。和伪目标不同的是:这个目标可以是一个存在的文件,但文件的具体内容我们并不关心,通常此文件是一个空文件。

空目标文件只是用来记录上一次执行的此规则的命令的时间。在这样的规则中,命令部分都会使用 "touch" 在完成所有的命令之后来更新目标文件的时间戳,记录此规则命令的最后执行时间。make 时通过命令行将此目标作为终极目标,当前目标下如果不存在这个文件,"touch" 会在第一次执行时创建一个的文件。

通常,一个空目标文件应该存在一个或者多个依赖文件。将这个目标作为终极目标,在它所依赖的文件比它更新时,此目标所在的规则的命令行将被执行。就是说如果空目标文件的依赖文件被改变之后,空目标文件所在的规则中定义的命令会被执行。看一个例子:

| .PHONY: | 这个目标的所有依赖被作为伪目标。伪目标是这样一个目标:当使用 make 命令行指定此目标时,这个目标所在的规则定义的命令、无论目标文件是否存在都会被无条件执行。 |
| .SUFFIXES: | 这个目标的所有依赖指出了一系列在后缀规则中需要检查的后缀名 |
| .DEFAULT: | Makefile 中,这个特殊目标所在规则定义的命令,被用在重建那些没有具体规则的目标,就是说一个文件作为某个规则的依赖,却不是另外一个规则的目标时,make 程序无法找到重建此文件的规则,这种情况就执行 ".DEFAULT" 所指定的命令。 |
| .PRECIOUS: | 这个特殊目标所在的依赖文件在 make 的过程中会被特殊处理:当命令执行的过程中断时,make 不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。 |
| .INTERMEDIATE: | 这个特殊目标的依赖文件在 make 执行时被作为中间文件对待。没有任何依赖文件的这个目标没有意义。 |
| .SECONDARY: | 这个特殊目标的依赖文件被作为中过程的文件对待。但是这些文件不会被删除。这个目标没有任何依赖文件的含义是:将所有的文件视为中间文件。 |
| .IGNORE | 这个目标的依赖文件忽略创建这个文件所执行命令的错误,给此目标指定命令是没有意义的。当此目标没有依赖文件时,将忽略所有命令执行的错误。 |
| .LOW_RESOLUTION_TIME: | 这个目标的依赖文件被 make 认为是低分辨率时间戳文件,给这个目标指定命令是没有意义的。通常的目标都是高分辨率时间戳。 |
| .SILENT: | 出现在此目标 ".SILENT" 的依赖文件列表中的文件,make 在创建这些文件时,不打印出此文件所执行的命令。同样,给目标 "SILENT" 指定命令行是没有意义的。 |
| .EXPORT_ALL_VARIABLES: | 此目标应该作为一个简单的没有依赖的目标,它的功能是将之后的所有变量传递给子 make 进程。 |
| .NOTPARALLEL: | Makefile 中如果出现这个特殊目标,则所有的命令按照串行的方式执行,即使是存在 make 的命令行参数 "-j" 。但在递归调用的子make进程中,命令行可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将会被忽略。 |</byte-sheet-html-origin>

Makefile 中,一个文件可以作为多个规则的目标。这种情况时,以这个文件为目标的规则的所有依赖文件将会被合并成此目标一个依赖文件列表,当其中的任何一个依赖文件比目标更新时,make 将会执行特定的命令来重建这个目标。

对于一个多规则的目标,重建这个目标的命令只能出现在一个规则中。如果多个规则同时给出重建此目标的命令,make 将使用最后一个规则中所定义的命令,同时提示错误信息。某些情况,需要对相同的目标使用不同的规则中所定义的命令,我们需要使用另一种方式——双冒号规则来实现。

一个仅仅描述依赖关系的描述规则可以用来给出一个或者时多个目标文件的依赖文件。例如,Makefile 中通常存在一个变量,就像我们以前提到的 "objects" ,它定义为所有的需要编译的生成 .o 文件的列表。这些 .o 文件在其源文件中包含的头文件 "config.h" 发生变化之后能够自动的被重建,我们可以使用多目标的方式来书写 Makefile:

这样做的好处是:我们可以在源文件增加或者删除了包含的头文件以后不用修改已存在的 Makefile 的规则,只需要增加或者删除某一个 .o 文件依赖的头文件。这种方式很简单也很方便。

我们也可以通过一个变量来增加目标的依赖文件,使用 make 的命令行来指定某一个目标的依赖头文件,例如:

它的意思是:如果我们执 "make exteradeps=foo.h" 那么 "foo.h" 将作为所有的 .o 文件的依赖文件。当然如果只执行 "make" 的话,就没有指定任何文件作为 .o 文件的依赖文件。

我们定义变量的目的是为了简化我们的书写格式,代替我们在代码中频繁出现且冗杂的部分。它可以出现在我们规则的目标中,也可以是我们规则的依赖中。我们使用的时候会经常的对它的值(表示的字符串)进行操作。遇到这样的问题我们可能会想到我们的字符串操作函数,比如 "patsubst" 就是我们经常使用的。但是我们使用变量同样可以解决这样的问题,我们通过下面的例子来具体的分析一下。

这段代码实现的功能是字符串的后缀名的替换,把变量 foo 中所有的以 .c 结尾的字符串全部替换成 .o 结尾的字符串。我们在 Makefile 中这样写,然后再 shell 命令行执行 make 命令,就可以看到打印出来的是 "a.o b.o d.o" ,实现了文件名后缀的替换。注意:括号中的变量使用的是变量名而不是变量名的引用,变量名的后面要使用冒号和参数选项分开,表达式中间不能使用空格。第二个变量 obj 是对整体的引用。

上面的例子我们可以换一种更加通用的方式来写,代码展示如下:

我们在 shell 中执行 make 命令,发现结果是相同的。

对比上面的实例我们可以看到,表达式中使用了 "%" 这个字符,这个字符的含义就是自动匹配一个或多个字符。在开发的过程中,我们通常会使用这种方式来进行变量替换引用的操作。

为什么这种方式比第一种方式更加实用呢?我们在实际使用的过程中,我们对变量值的操作不只是修改其中的一个部分,甚至是改变其中的多个,那么第一种方式就不能实现了。我们来看一下这种情况:

我们可以看到这个例子中我们操作的是两个不连续的部分,我们执行 make 后打印的值是 "x123y x1234y x12345y",这种情况下我们使用第一种情况就不能实现,所以第二种的使用更全面。

变量的嵌套引用的具体含义是这样的,我们可以在一个变量的赋值中引用其他的变量,并且引用变量的数量和和次数是不限制的。下面我们通过几个实例来说明一下。

这种用法是最常见的使用方法,打印出 var 的值就是 test。我们可以认为是一层的嵌套引用。

我们再去执行 make 命令的时候得到的结果也是 test,我们可以来分析一下这段代码执行的过程:$(foo) 代表的字符串是 bar,我们也定义了变量 bar,所以我们可以对 bar 进行引用,变量 bar 表示的值是 test,所以对 bar 的引用就是 test,所以最终 var 的值就是 test。这是变量的二层嵌套执行,当然我们还可以使用三层的嵌套执行,写法跟上面的方式是一样的。嵌套的层数也可以更多,但是不提倡使用。

我们再去使用变量的时候,我们并不是只能引用一个变量,可以有多个变量的引用,还可以包含很多的变量还可以是一些文本字符。我们可以通过一些例子来说明一下。

在命令行执行 make 我们可以得到 var 的值是 hello。这是变量嵌套引用的时候可以包含其它字符的使用情况。

这个实例跟上面实例的运行结果是一样的。我们可以看到这个实例中使用了两个变量的引用还有其它的字符。

变量的嵌套引用和我们的变量的递归赋值的区别:嵌套引用的使用方法就是用一个变量表示另外一个变量,然后进行多层的引用。而递归展开的变量表示当一个变量存在对其它变量的引用时,对这变量替换的方式。递归展开在另外一个角度描述了这个变量在定义是赋予它的一个属性或者风格。并且我们可以在定义个一个递归展开式的变量时使用套嵌引用的方式,但是建议你的实际编写 Makefile 时要尽量避免这种复杂的用法。

在实际使用的过程中变量的第一种用法经常使用的,第二种用法我们很少使用,应该说是尽量避免使用变量的嵌套引用。在必须要使用的时候我们应该做到嵌套的层数是越少越好的。因为使用这种方法表达会比较的复杂,如果条理不清楚的话我们就会出错。并且在给其他人看的时候也会不容易理解。

Makefile 中提供了两个控制 make 运行方式的函数。其作用是当 make 执行过程中检测到某些错误时为用户提供消息,并且可以控制 make 执行过程是否继续。这两个函数是 "error" 和 "warning",我们来详细的介绍一下这两个函数。

  • 函数功能:产生致命错误,并提示 "TEXT..." 信息给用户,并退出 make 的执行。需要说明的是:"error" 函数是在函数展开时(函数被调用时)才提示信息并结束 make 进程。因此如果函数出现在命令中或者一个递归的变量定义时,读取 Makefile 时不会出现错误。而只有包含 "error" 函数引用的命令被执行,或者定义中引用此函数的递归变量被展开时,才会提示知名信息 "TEXT..." 同时退出 make。

  • 函数说明:"error" 函数一般不出现在直接展开式的变量定义中,否则在 make 读取 Makefile 时将会提示致命错误。

我们通过两个例子来说明一下;

这个例子,在 make 读取 Makefile 时不会出现致命错误。只有目标 "err" 被作为是一个目标被执行时才会出现。

  • 函数功能:函数 "warning" 类似于函数 "error" ,区别在于它不会导致致命错误(make不退出),而只是提示 "TEXT...",make 的执行过程继续。

  • 函数说明:用法和 "error" 类似,展开过程相同。

make 执行过程的致命错误都带有前缀字符串 "***"。错误信息都有前缀,一种是执行程序名作为错误前缀(通常是 "make");另外一种是当 Makefile 本身存在语法错误无法被 make 解析并执行时,前缀包含了 Makefile 文件名和出现错误的行号。

在下述的错误列表中,省略了普通前缀:

这类错误并不是 make 的真正错误。它表示 make 检测到 make 所调用的作为执行命令的程序返回一个非零状态(Error NN),或者此命令程序以非正常方式退出(携带某种信号)。

如果错误信息中没有附加 "***" 字符串,则是子过程的调用失败,如果 Makefile 中此命令有前缀 "-",make 会忽略这个错误。

错误的原因:不可识别的命令行,make 在读取 Makefile 过程中不能解析其中包含的内容。GNU make在读取 Makefile 时根据各种分隔符(:, =, [TAB]字符等)来识别 Makefile 的每一行内容。这些错误意味着 make 不能发现一个合法的分隔符。

出现这些错误信息的可能的原因是(或许是编辑器,绝大部分是ms- windows的编辑器)在 Makefile 中的命令之前使用了4个(或者8个)空格代替了 [Tab] 字符。这种情况,将产生上述的第二种形式产生错误信息。且记,所有的命令行都应该是以 [Tab] 字符开始的。

Makefile 可能是以命令行开始:以 [Tab] 字符开始,但不是一个合法的命令行(例如,一个变量的赋值)。命令行必须和规则一一对应。

产生第二种的错误的原因可能是一行的第一个非空字符为分号,make 会认为此处遗漏了规则的 "target: prerequisite" 部分。

无法为重建目标“XXX”找到合适的规则,包括明确规则和隐含规则。

修正这个错误的方法是:在 Makefile 中添加一个重建目标的规则。其它可能导致这些错误的原因是 Makefile 中文件名拼写错误,或者破坏了源文件树(一个文件不能被重建,可能是由于依赖文件的问题)。

第一个错误表示在命令行中没有指定需要重建的目标,并且 make 不能读入任何 Makefile 文件。第二个错误表示能够找到 Makefile 文件,但没有终极目标或者没有在命令行中指出需要重建的目标。这种情况下,make 什么也不做。

对同一目标 "XXX" 存在一个以上的重建命令。GNU make 规定:当同一个文件作为多个规则的目标时,只能有一个规则定义重建它的命令(双冒号规则除外)。如果为一个目标多次指定了相同或者不同的命令,就会产生第一个告警;第二个告警信息说新指定的命令覆盖了上一次指定的命令。

make 的变量 "XXX"(递归展开式)在替换展开时,引用它自身。无论对于直接展开式变量(通过:=定义的)或追加定义(+=),这都是不允许的。

变量或者函数引用语法不正确,没有使用完整的的括号(缺少左括号或者右括号)。

函数 "XXX" 引用时参数数目不正确。函数缺少参数。

不正确的静态模式规则。

第一条错误的原因是:静态模式规则的目标段中没有模式目标;

第二条错误的原因是:静态模式规则的目标段中存在多个模式目标;

第三条错误的原因是:静态模式规则的目标段目标模式中没有包含模式字符“%”;

第四条错误的原因是:静态模式规则的三部分都包含了模式字符“%”。正确的应该是只有后两个才可以包含模式字符“%”。

这一条告警和下条告警信息发生在:make 检测到递归的 make 调用时,可通信的子 make 进程出现并行处理的错误。递归执行的 make 的命令行参数中存在 "-jN" 参数(N的值大于1),在有些情况下可能导致此错误,例如:Makefile 中变量 "MAKE" 被赋值为 "make –j2",并且递归调用的命令行中使用变量 "MAKE"。在这种情况下,被调用 make

为了现实 make 进程之间的通信,上层 make 进程将传递信息给子 make 进程。在传递信息过程中可能存在这种情况,子 make 进程不是一个实际的 make 进程,而上层make却不能确定子进程是否是真实的 make 进程。它只是将所有信息传递下去。上层 make 采用正常的算法来决定这些。当出现这种情况,子进程只会接受父进程传递的部分有用的信息。子进程会产生该警告信息,之后按照其内建的顺序方式进行处理。

1、在main执行之前和之后执行的代码可能是什么?

main函数执行之前,主要就是初始化系统相关资源:

初始化静态static变量和global全局变量,即.data段的内容

将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容

全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码

将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数

main函数执行之后:

全局对象的析构函数会在main函数之后执行;

可以用 atexit 注册一个函数,它会在main 之后执行;

2、结构体内存对齐问题?

结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。

未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)

指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名

指针可以有多级,引用只有一级

指针可以为空,引用不能为NULL且在定义时必须初始化

指针在初始化后可以改变指向,而引用在初始化之后不可再改变

sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小

当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。

引用只是别名,不占用具体存储空间,只有声明没有定义;指针是具体变量,需要占用存储空间。

引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。

引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。

不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。

申请方式不同:栈由系统自动分配;堆是自己申请和释放的。

申请大小限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改;堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。

申请效率不同:栈由系统分配,速度快,不会有碎片;堆由程序员分配,速度慢,且会有碎片。

栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

《C++中堆(heap)和栈(stack)的区别》:

5、区别以下指针类型?

int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。

int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。

int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。

int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

6、基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

首先整理一下虚函数表的特征:

虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成

虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段

虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中

根据以上特征,虚函数表类似于类中静态成员变量。静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,结果显示:

虚函数表vtable在/Unix中存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别

由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。

一般分为五个区域:栈区、堆区、函数区(存放函数体等二进制代码)、全局静态区、常量区

C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。

都可用于内存的动态申请和释放

前者是C++运算符,后者是C/C++语言标准库函数

new自动计算要分配的空间大小,malloc需要手工计算

new是类型安全的,malloc不是。例如:

new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。后者均没有相关调用

后者需要库文件支持,前者不用

new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针

delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存

malloc和free是标准库函数,支持覆盖;new和delete是运算符,并且支持重载。

malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。

malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

delete只会调用一次析构函数。

delete[]会调用数组中每个元素的析构函数。

10、宏定义和函数有何区别?

宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。

宏定义属于在结构中插入代码,没有返回值;函数调用具有返回值。

宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。

宏定义不要在最后加分号。

宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。

宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。

宏不检查类型;typedef会检查数据类型。

宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。

12、变量声明和定义区别?

声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。

相同变量可以在多处声明(外部变量extern),但只能在一处定义。

13、哪几种情况必须用到初始化成员列表?

初始化一个const成员。

调用一个基类的构造函数,而该函数有一组参数。

调用一个数据成员对象的构造函数,而该函数有一组参数。

sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。

sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是指针且结尾是‘’的字符串。

因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。

15、常量指针和指针常量区别?

常量指针是一个指针,读成常量的指针,指向一个只读变量。如int const *p或const int *p。

指针常量是一个不能给改变指向的指针。指针是个常亮,不能中途改变指向,如int *const p。

a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x,加1操作后变为0x。*(a + 1) = a[1]。

&a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。

若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

17、数组名和指针(这里为指向数组首元素的指针)区别?

二者均可通过增减偏移量来访问数组中的元素。

数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

18、野指针和悬空指针

都是是指向无效内存区域(这里的无效指的是“不安全不可控”)的指针,访问行为将会导致未定义行为。

野指针,指的是没有被初始化过的指针

因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。

悬空指针,指针最初指向的内存已经被释放了的一种指针。

此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。

避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。

野指针:指针变量未及时初始化 =》 定义指针变量及时初始化,要么置空。

悬空指针:指针free或delete之后没有及时置空 =》 释放操作后立即置空。

19、迭代器失效的情况

1、尾后插入:size 《 capacity时,首迭代器不失效尾迭代失效(未重新分配空间),size == capacity时,所有迭代器均失效(需要重新分配空间)。

2、中间插入:中间插入:size 《 capacity时,首迭代器不失效但插入元素之后所有迭代器失效,size == capacity时,所有迭代器均失效。

尾后删除:只有尾迭代失效。

中间删除:删除位置之后所有迭代失效。

标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数(C中没有字符串类型)。

C++中用来做控制态输入输出的tream类库替代了标准C中的stdio函数库。

在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。也就是C++可以重载,C语言不允许。

C++语言中,允许变量定义语句在程序中的任何地方,只要在是使用它之前就可以;而C语言中,必须要在函数开头部分。而且C++允许重复定义变量,C语言也是做不到这一点的

在C++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。

《C语言与C++有什么区别?》

define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用

define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错

const常量有数据类型,编译器可以对其进行类型安全检查

define只是将宏名称进行替换,在内存中会产生多分相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表

宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。

宏不检查类型;const会检查数据类型。

宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。

隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用

默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区

静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用

static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。

static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问

const常量在定义时必须初始化,之后无法更改

const形参可以接收const和非const类型的实参,例如

const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化

const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值

顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边

底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边

执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const

使用命名的强制类型转换函数const_cast时,只能改变运算对象的底层const

int *const a,依旧是指针类型,表示a为指向整型数据的常指针。(看成const(a),对指针const)

26、类的对象存储空间?

非静态成员的数据类型大小之和。

编译器加入的额外成员变量(如指向虚函数表的指针)。

为了边缘对齐优化加入的padding。

当在父类中使用了虚函数时候,你可能需要在某个子类中对这个虚函数进行重写,以下方法都可以:

如果不使用override,当你手一抖,将foo()写成了foo()会怎么样呢?结果是编译器并不会报错,因为它并不知道你的目的是重写虚函数,而是把它当成了新的函数。如果这个虚函数很重要的话,那就会对整个程序不利。所以,override的作用就出来了,它指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的:

当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。例子如下:

29、初始化和赋值的区别

对于简单类型来说,初始化和赋值没什么区别

对于类和复杂数据类型来说,这两者的区别就大了,举例如下:

综上,总结出使用方法,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern “C”声明,在.c文件中包含了extern “C”时会出现编译语法错误。所以使用extern “C”全部都放在于cpp程序相关文件或其头文件中。

(1)C++调用C函数:

38、浅拷贝和深拷贝的区别

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

39、内联函数和宏定义的区别

内联(inline)函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接嵌入到目标代码中。

使用宏定义的地方都可以使用inline函数

作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率

为什么不能把所有的函数写成内联函数

内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

函数体内的代码比较长,将导致内存消耗代价

函数体内有循环,函数执行时间要比函数调用开销大

内联函数在编译时展开,宏在预编译时展开

内联函数直接嵌入到目标代码中,宏是简单的做文本替换

内联函数有类型检测、语法判断等功能,而宏没有

内联函数是函数,宏不是

宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义

内联函数代码是被放到符号表中,使用时像宏一样展开,没有调用的开销,效率很高;

在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。

内联函数本身是函数,强调函数特性,具有重载等功能。

内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员,进而提升效率。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了。

40、构造函数、析构函数、虚函数可否声明为内联函数

首先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联。

register关键字:这个关键字请求编译器尽可能的将变量存在内部寄存器中,而不是通过内存寻址访问,以提高效率

言下之意就是普通的new,就是我们常用的new,在C++中定义如下:

因此plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的,举个例子:

nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:

这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:

palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组

placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

算是为了与C语言进行兼容而定义的一个问题吧

NULL来自C语言,一般由宏定义实现,而 nullptr 则是C++11的新增关键字。在C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数0。编译器一般对其实际定义如下:

在C++中指针必须有明确的类型定义。但是将NULL定义为0带来的另一个问题是无法与整数的0区分。因为C++中允许有函数重载,所以可以试想如下函数定义情况:

那么在传入NULL参数时,会把NULL当做整数0来看,如果我们想调用参数是指针的函数,该怎么办呢?。nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。

nullptr的一种实现方式如下:

以上通过模板类和运算符重载的方式来对不同类型的指针进行实例化从而解决了(void*)指针带来参数类型不明的问题,另外由于nullptr是明确的指针类型,所以不会与整形变量相混淆。但nullptr仍然存在一定问题,例如:

在这种情况下存在对不同指针类型的函数重载,此时如果传入nullptr指针则仍然存在无法区分应实际调用哪个函数,这种情况下必须显示的指明参数类型。

48、简要说明C++的内存分区

C++中的内存分区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区和代码区。如下图所示

栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限

堆:就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收

自由存储区:就是那些由malloc等分配的内存块,它和堆是十分相似的,不过它是用free来结束自己的生命的

全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量和静态变量又分为初始化的和未初始化的,在C++里面没有这个区分了,它们共同占用同一块内存区,在该区定义的变量若没有初始化,则会被自动初始化,例如int型变量自动初始为0

常量存储区:这是一块比较特殊的存储区,这里面存放的是常量,不允许修改

代码区:存放函数体的二进制代码

49、C++的异常处理的方法

在程序执行过程中,由于程序员的疏忽或是系统资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:

动态分配空间时空间不足

如果不及时对这些异常进行处理,程序多数情况下都会崩溃。

C++中的异常处理机制主要使用try、throw和catch三个关键字,其在程序中的用法如下:

代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块。如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。

catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。

当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。

(2)函数的异常声明列表

有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出异常的列表,写法如下:

这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常

C++ 标准库中有一些类代表异常,这些类都是从 exception 类派生而来的,如下图所示

bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常,例如:

bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常

bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常

out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

原文标题:《逆袭进大厂》之 C++ 篇 49 问 49 答(绝对的干货)

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

我要回帖

更多关于 datepart函数的使用 的文章

 

随机推荐