系统摄像视频文件格式
一般手机使用摄像头录制视频格式为yuv420p,而小米5录制出的为yuvj420p.格式转换是yuvj420p当成yuv420p处理即可.
问题:
1 | Users/shenjunwei/program/android-ndk-r14b/build/core/build-binary.mk:687: Android NDK: Module magicsdk_fmod depends on undefined modules: cutils |
解决方案:
Android.mk中增加APP_ALLOW_MISSING_DEPS=true
问题:
1 | /Users/shenjunwei/program/android-ndk-r14b/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9.x/../../../../arm-linux-androideabi/bin/ld: warning: shared library text segment is not shareable |
解决:
1 | from Android NDK r11 you can use |
问题:
1 | /data/app/com.wonxing.touchfa-2/lib/arm/libmagicsdk_ex.so: has text relocations |
解决:
Using “22” and not “23” as targetSDKVersion solved it. (See below)
1 | <uses-sdk |
I also checked the build.gradle files for compile version and targetSDKversion:
1 | compileSdkVersion 22 |
1 | libavcodec\arm\fft_fixed_neon.S |
I took the latest from https://github.com/FFmpeg/FFmpeg
You will also need HAVE_SECTION_DATA_REL_RO declared somewhere in your build for the macro in asm.S to use the dynamic relocations option.
“libfoo.so has text relocations. This is wasting memory and prevents security hardening. Please fix.”.
Despite this, the OS will load the library anyway. Marshmallow rejects library if your app’s target SDK version is >= 23. System no longer logs this because it assumes that your app will log the dlopen(3) failure itself, and include the text from dlerror(3) which does explain the problem. Unfortunately, lots of apps seem to catch and hide the UnsatisfiedLinkError throw by System.loadLibrary in this case, often leaving no clue that the library failed to load until you try to invoke one of your native methods and the VM complains that it’s not present.
You can use the command-line scanelf tool to check for text relocations. You can find advice on the subject on the internet; for example https://wiki.gentoo.org/wiki/Hardened/Textrels_Guide is a useful guide.
And you can check if your shared lbirary has text relocations by doing this:
1 | readelf -a path/to/yourlib.so | grep TEXTREL |
If it has text relocations, it will show you something like this:
1 | 0x00000016 (TEXTREL) 0x0 |
If this is the case, you may recompile your shared library with the latest NDK version available:
1 | ndk-build -B -j 8 |
And if you check it again, the grep command will return nothing.
建雨在芝士圈应用的application中使用了全局静态变量标志是否正在录制中,开启直播后将该变量设置为录制中,录制中一些操作将被屏蔽.但是对某些手机(如htc d816)当从”最近应用”杀掉进程后有时候application不被回收,该状态变量无法通过application的onCreate中重新初始化,同时通知栏也未消失.在Android 应用被杀后Notification不取消问题及应用深杀和浅杀时Service生命周期情况探讨中找到service的onTaskRemoved方法可以监听到应用被从最近应用中移除.
关于<<Android 应用被杀后Notification不取消问题及应用深杀和浅杀时Service生命周期情况>>摘要: 目中有如下需求:后台service进行导入操作,要更新Notification。当运行系统清理使应用被杀时,Notification无法取消,仍然在通知栏显示。为解决这个问题进行了如下探索:
首先想到利用service的startForeground()来更新通知栏,这样当应用被杀掉时候Notification可以一起被去掉。但针对项目的需求:service可以同时导入多个文件,并且会对应显示多个通知。这种情况下用service.startForeground()更新通知栏时候,当应用被杀时候之后cancel掉最后一次调用startForeground对应id的Notification,而其他通知仍然不能被取消。
继续探索用其他方式取消通知栏:在进程被杀掉的时候,会调用service的哪些生命周期函数呢?service的onDestroy()方法只有在调用Context的stopService()或Service的stopSelf()后才会被调用,在应用被杀时候Service的onDestroy()不会被执行。
我们发现service的 onTaskRemoved()方法,该方法何时被调用呢?方法的注释说明是这么写的:
1 | /** |
注释表明onTaskRemoved()方法在当用户移除应用的一个Task栈时被调用。也就是当用户在最近任务界面把该应用的一个task划掉时,或者在最近任务界面进行清理时。这两种情况下onTaskRemoved()都会被调用,但在大多Android机型上,这两种情况有所不同:第一种情况即应用被浅杀(用户只划掉这一个Task),该Task栈会被清理,但如果有后台service在运行,该应用的进程不会被杀掉,后台service仍然在运行。第二种即应用被深杀(用户在最近任务界面直接按清理按钮),该应用的进程会被直接杀掉,后台的service当然也停止了。对于不同的手机品牌和机型在最近任务进行各种清理时过程可能不太一样,但应用浅杀和深杀对于所有Android手机都是有普遍意义的。
下面我们分析在应用被浅杀和被深杀以及先浅杀再深杀后的生命周期:
浅杀:
1 | 04-21 17:55:13.733 8264-8264/com.qintong.test D/qintong: vCardService onTaskRemoved. |
深杀: 会出现两种情况: (a).
1 | 04-26 16:20:00.349 32674-32674/? D/qintong: Service onTaskRemoved. |
(b).
1 | 04-21 17:59:58.397 8264-8264/com.qintong.test D/qintong: Service onCreate. |
浅杀+深杀 (service 的 onStartCommand 返回 STICKY):
1 | 04-21 18:05:12.717 8264-8264/com.qintong.test D/qintong: Service onTaskRemoved. |
我们来分析这几种情况: (1).浅杀时:应用进程没被杀掉,service仍然在执行,service的onTaskRemoved()立即被调用。
(2).深杀时:有两种情况:第一种情况是深杀后直接调用onTaskRemoved()且service停止,过段时间后service重启调用其onCreate()和onStartCommand()。第二种是应用的进程被杀掉,过一会后service的onCreate()方法被调用,紧接着onTaskRemoved()被调用。由于被深杀后应用的进程立刻停止了,所以service的onTaskRemoved()无法被立即调用。而过若干秒后,service重启,onCreate()被调用,紧接着onTaskRemoved()被调用。而这里service的其他方法并没有被调用,即使onStartCommand()返回STICKY,service重启后onStartCommand()方法也没有被调用。
(3).浅杀+深杀时(service 的 onStartCommand 返回 STICKY):onTaskRemoved()立刻被调用(浅杀后),深杀后过段时间onCreate()和onStartCommand()相继被调用。执行浅杀Task被清理,应用的进程还在,onTaskRemoved()被调用,过程与(1)一样。再执行深杀:由于该应用的Task栈已经没有了,所有再深杀onTaskRemoved()不会再被调用,深杀后service停止。而由于实验时候onStartCommand()返回STICKY,所有service过段时间会被再次启动,执行了onCreate()方法和onStartCommand()方法。
所以综上所述,service的onTaskRemoved()在应用浅杀后会被立即调用而在service被深杀后,会直接调用onTaskRemoved或service会被重启并调用onTaskRemoved()。
回到我们的问题:应用被杀后,如何取消Notification: 我们先看最后的解决方案,在来分析为何能work。 service的代码如下:
1 | @Override |
如上代码,在浅杀时候:只执行onTaskRemoved(),通知被取消,但service仍然在运行,所以还会继续发通知,正常运行。 深杀时:第一种情况直接调用onTaskRemoved()且service停止,通知被取消。第二种情况,进程被杀掉,几秒后service重启,onCreate() -> onTaskRemoved(),运行结果就是深杀后过几秒后Notification被取消。 浅杀+深杀时:浅杀后onTaskRemoved()被调用,service仍在运行,通知仍然在更新。深杀时,onCreate() -> onStartCommand(),在onStartCommand()时候取消通知。 另外,mNotificationManager.cancelAll()会清除应用的所有通知,如果应用想保留和该service无关其他通知,可以调用mNotificationManager.cancel(String tag, int id)或cancel(int id)清除指定通知。 当然,还可以有另一种方式:浅杀时后就把service后台执行的任务停止,并清理notification,我们可以根据需求来选择。
补充: 疑问:1.为啥有时候深杀不立即调用onTaskRemoved(),而是在重启之后调用的呢? stackoverflow上的答复:https://stackoverflow.com/questions/32224233/ontaskremoved-called-after-oncreate-in-started-service-on-swipe-out-from-recent/41506752 大意是service执行较重UI操作时候service不会立即停止,而新的service会启动。不太确定这个解释的正确性……
Calling onTaskRemoved of the running service(when app gets swiped out from recent apps) will be generally delayed if we are performing any heavy UI related stuff or broadcasting messages to receivers in service. E.g , Assume you are downloading the file of size 50MB from web server, so from web server everytime you are reading 1024bytes of stream data as buffer and that data you are writing to a file in device. Meanwhile you are updating the progress to the UI thread which means every KB you are updating to the UI thread, this will cause the application to freeze. So in between if you swipe-out from recent app list , then the system will try to stop the service but since the service is in-contact with the UI thread, the system will be unable to stop that service, but it will create new service eventhough the old service is not yet stopped. Once old service finishes the communication with the UI thread then onTaskRemoved() gets called and the old service will be stopped. The new service will be running in the background. 2.为何servive.startForeground()添加的Notification可以在service被杀死后去掉呢?我们分析源码:ActiveServices中killServicesLocked()->scheduleServiceRestartLocked()中调用了r.cancelNotification(),清除了notification:
1 | public void cancelNotification() { |
1 | var str1 = "chaychan" |
比较两个字符串,如果两个字符串的内容一致,在Java中使用 str1 == str2 时,是比较两个字符串的地址值,很清楚两个字符串的地址不一样,返回false,但是在kotlin中,则不是如此,比较的只是字符串的内容,而===相当于Java中的==,用来比较引用对象, 上述代码返回的是true。
equal函数
equals(str:String)
方法中的参数是与之对比的字符串,默认不忽略大小写,即大小写敏感,比如:
1 | var str1 = "chaychan" |
打印结果为false,因为不忽略大小写的话,两个字符串内容对比是不一致的,所以返回false。
equals(str:String,ignoreCase:Boolean)
方法中有两个参数,第一个参数是与之对比的字符串,第二个参数是布尔类型的值,是否忽略大小写,如:1 | var str1 = "chaychan" |
返回结果为true。
在View声明阶段,都会需要使用lateinit来延迟声明变量。
1 | class TaskActivity : AppCompatActivity(){ |
kotlin中延迟声明还包括lazy的方式
1 | val name:String by lazy {"cangwang"} |
区别在于:
lateinit var name:String
会报错Api26前:
1 | @Override |
Api26之后
1 | @SuppressWarnings("TypeParameterUnusedInFormals") |
1 | setSupportActionBar(findViewById<Toolbar>(R.id.toolbar)) |
在函数内可以通过this指代该对象,返回值为该对象自己
1 | override fun getView(i:Int,view:View?,viewGroup:ViewGroup):View{ |
1 | private fun showMessage(message:String){ |
将对象作为函数参数,在函数块内可以通过it指代该对象.返回值为函数块的最后一行或指定return表达式
其有两种表达式:
单例对象是使用Object申明 Kotlin没有静态属性和方法,需要使用单例对象来实现类似的功能.
相当于java中定义的数据bean类,其可以直接在属性之后编写get和set方法
@JvmOverloads
看到这个标题,你可能非常惊讶,C语言也能实现泛型链表?我们知道链表是我们非常常用的数据结构,但是在C中却没有像C++中的STL那样有一个list的模板类,那么我们是否可以用C语言实现一个像STL中的list那样的泛型链表呢?答案是肯定的。下面就以本人的一个用C语言设计的链表为例子,来分析说明一下本人的设计和实现要点,希望能给你一点有用的帮助。
我们知道,链表也有非常多的类型,包括单链表、单循环链表、双链表、双向循环链表等。在我的设计中,我的链表使用的类型是双向循环链表,并带一个不保存真实数据的头结点。其原因如下: 1)单链表由于不能从后继定位到前驱,在操作时较为不方便 2)双链表虽然能方便找到前驱,但是如果总是在其尾部插入或删除结点,为了定位的方便和操作的统一(所有的删除和插入操作,都跟在中间插入删除结点的操作一样),还要为其增加一个尾结点,并且程序还要保存一个指向这个尾结点的指针,并管理这个指针,从而增加程序的复杂性。而使用带头结点的循环双向链表,就能方便的定位(其上一个元素为链表的最后一个元素,其下一个元素为链表的第0个元素),并使所有的插入和删除的操作统一,因为头结点也是尾结点。注:结点的下标从0开始,头结点不算入下标值。 3)接口的使用与C++中stl中list和泛型算法的使用大致相同。
为了让大家一睹为快,下面就给出这个用C语言实现的“泛型”的定义,再来说明,我这样设计的原因及要点,其定义如下: 其定义在文件list_v2.c中
1 | typedef struct node |
其声明在文件list_v2.h中
1 | //泛型循环双链表,带头结点,结点下标从0开始,头结点不计入下标值 |
首先,我们为什么需要封装呢?我觉得封装主要有三大好处。
在面向对象的设计中,如果我们想要隐藏一个类的成员变量,我们可以把这些成员变量声明为私有的,而在C语言中,我们可以怎么实现呢?其实其实现是很简单的,我们在C语言中,当我们要使用一个自己定义的类型或函数时,我们会把声明它的头文件包含(include)过来,只要我们在文件中只声明其类型是一个结构体,而把它的实现写在.c文件中即可。
在本例子中,我把struct list
和struct node
定义在.c文件中,而在头文件中,只声明其指针类型,即typedef struct node* Iterator
和typedef struct list* List;
当我们要使用该类型时,只需要在所在的文件中,include该头文件即可。因为在编译时,编译器只要知道List和Iterator是一个指针类型就能知道其所占的内存大小,也就能为其分配内存,所以能够编译成功。而又因为该头文件中并没有该类型(struct list和struct node)的定义,所以我们在使用该类型时,只能通过我们提供的接口来操作对象。例如,我们并不能使用List list; list->data
等等的操作,而只能通过已定义的接口GetData来获得。
###四、如何实现泛型
泛型,第一时间想起的可能是模板,但是在C语言中却没有这个东西。但是C语言中却有一个可以指向任何类型,在使用时,再根据具体的指针类型进行类型转换的指针类型,它就是void*
。
为什么void可以指向任何类型的数据?这还得从C语言对于数据类型的处理方式来说明。在C语言中,我们使用malloc
等函数来申请内存,而从内存的角度来看,数据是没有类型的,它们都是一串的0或1,而程序则根据不同的类型来解释这个内存单元中的数据的意义,例如对于内存中的数据,FFFFFFFF
,如果它是一个有符号整型数据,它代表的是-1
,而如果它是一个无符号整型数据,它代表的则是2^32-1
。进一步说,如果你用一个int
的指针变量p指向该内存,则p就是-1,如果你用unsigned int
的指针p
指向该内存,则*p = 2^32-1
。
而我们使用malloc
等函数时,也只需要说明申请的内存的大小即可,也不用说明申请的内存空间所存放的数据的类型,例如,我们申请一块内存空间来存放一个整型数据,则只需要malloc(sizeof(int))
,即可,当然你完全可以把它当作一个具有4个单位的char数组来使用。所以我们可以使用void指针来指向我们申请的内存,申请内存的大小由链表中的成员data_size定义,它也是真正的data所占的内存大小。
这里来说明一下,该链表的数据的插入方式,我们的插入方式是,新建一个结点,把data指向的数据复制到结点中,并把该结点插入到链表中。插入的函数定义如下:
1 | Iterator Insert(List list, void *data, Iterator it_before, |
从上面的解说中,我们可以看到链表中的成员data_size指示了链表中的数据所占的内存大小,那我们们就可以使用函数memcpy把data指向的数据复制到新建的结点的data所指向的内存即可。为什么还需要一个函数指针assign,来指向一个定义数据之间如何赋值的函数呢?其实这和面向对象语言中常说到的深复制和浅复制有关。
注:memcpy函数的原型为:void * memcpy ( void * destination, const void * source, size_t num );
试想一下,假如你的链表的数据类型不是int型等基本类型,也不是不含有指针的结构体,而是一个这样的结构体,例如:
1 | struct student |
学生的姓名和学号都是能过动态分配内存而来的,并由student结构体中的name和no指针指向,那么当我们使用memcpy时,只能复制其指针,而不能复制其指向的数据,这样在很多情况下都会带来一定的问题。这个跟在C++中什么时候需要自己定义复制构造函数的情况类似。因为这种情况下,默认的复制构造函数并不能满足我们的需要,只能自己定义复制构造函数。
所以在插入一个结点时,需要assign函数指针的原理与C++中自己定义复制构造函数的原理一样。它用于定义如何根据一个已有的对象生成一个该对象的拷贝对象。当然,可能在大多数的情况下,我们需要用到的数据类型都没有包含指针,所以在Insert函数的实现中,其实我也是有用到memcpy函数的,就是当assign为NULL时,就使用memcpy函数进行数据对象间的赋值,它其实就相当于C++中的默认复制构造函数或默认赋值操作函数。assign为NULL表示使用默认的逐位复制方式,即浅复制。
对于这个问题,其实很好回答。很多人实现一个通用链表是这样实现的,它们把node结构的实现如下:
1 | typedef struct node |
然后,当需要使用整型的链表时,就把DataType用typedef为int。其实这样做的一个最大的缺陷就是一个程序中只能存在着一个数据类型的链表,例如,如果我需要一个int型的链表和一个float型的链表,那么该把DataType定义为int呢还是float呢?所以这种看似可行的方式,其实只是虚有其表,在现象中是行不能的,虽然不少的数据结构的书都是这样实现的,但是它却没有什么实用价值。
而其本质的原因是把结点的数据域的数据类型与某一种特定的数据类型DataType绑定在一起,从而让链表不能独立地变化。
在C++中iterator是一个类,为什么在这里,我只把结点的指针声明为一个Iterator呢?其实受STL的影响,我在一开始时,也是把Iterator实现为一个结构体,它只有一个数据成员,就是一个指向Node的指针。但在后来的实践中,发现其实并没有必要。在C++中为什么把iterator定义为一个类,是为了重载*,->等运行符,让iterator使用起来跟普通的指针一样。但是在C语言中,并没有重载运行符的做法,所以直接把Ierator声明为一个Node的指针最为方便、直接和好用,所有的比较运算都可以直接进行,而无需要借助函数。而把它声明为一个结构体反而麻烦、累赘。
其实这是参考了STL中的泛型算法的思想。而且本人觉得这是一种比较好的实现。为什么FindFirst的函数原型不是
1 | Iterator FindFirst(List list, int (*condition)(const void*, const void*)); |
而是
1 | Iterator FindFirst(Iterator begin, Iterator end, void *data,int (*condition)(const void*, const void*)); |
们可以试想一下,这个链表的为char链表,链表的元素为ABCBCBC,我们要在链表中找出所有的B,如果查找算法是使用第一种定义的话,它只能找出第一个B,而后面的两个B就无能为力了,而第二种定义,则可以通过循环改变其始末迭代器来在不同的序列段间查找目标字符B的位置。
由于AS3.0默认支持Java8语言,所以我们就可以移除build.gradle里面的jackOptions了
jackOptions { true }
然后可以在build.gradle配置为Java8
1 | android { |
如果对Java8的一些特性存在问题,我们也可以在gradle.properties里面禁用Java8
1 | android.enableDesugar=false |
AS3.0以前我们常用productFlavors配置不同的渠道包,比如
1 | productFlavors { |
AS3.0得新增flavorDimensions的配置,主要有以下 12 个构建变体: 构建变体:
1 | [minApi24, minApi23, minApi21][Demo, Full][Debug, Release] |
对应 APK:
1 | app-[minApi24, minApi23, minApi21]-[demo, full]-[debug, release].apk |
比如这里创建一个构建方式 首先得在defaultConfig通过flavorDimensions配置构建变体,如下
1 | defaultConfig { |
然后productFlavors的配置就可以如下:
1 | productFlavors { |
android.enableAapt2=true
1 | dependencies{ |
1 | Error:Cause: getMainOutputFile is no longer supported. Use getOutputFileName if you need to determine the file name of the output. |
或
1 | Error:Not valid. |
主要是AndResGuard1.2.3版本还没有兼容AS3.0
1 | Error:All flavors must now belong to a named flavor dimension. The flavor 'prod' is not assigned to a flavor dimension. Learn more at https://d.android.com/r/tools/flavorDimensions-missing-error-message.html |
AS3.0需要通过flavorDimensions来配置产品渠道,详细看上文。
INSTALL_FAILED_TEST_ONLY
adb install 安装debug后提示INSTALL_FAILED_TEST_ONLY,原来是Android Studio 3.0会在debug apk的manifest文件application标签里自动添加 android:testOnly=”true”属性,提示错误:
1 | apk adb install '/home/silver/桌面/share/apk/app-android-debug.apk' |
但是使用Android Studio开发过程中发现可以直接安装成功。经过查询资料发现在AndroidManifest.xml文件中添加了属性testOnly=true,
https://developer.android.com/guide/topics/manifest/application-element
反编译当前apk发现的确清单文件中的确新加了这个属性,研究发现原来是Android Studio 3.0会在debug apk的manifest文件application标签里自动添加 android:testOnly=”true”属性,导致IDE中run跑出的apk在大部分手机上只能用adb install -t 来安装。 也可以在项目中的gradle.properties全局配置中设置:
1 | android.injected.testOnly=false |
1 | Error:Execution failed for task ':app:transformClassesWithExtractJarsForDebug'. |
解决方案: 造成上述问题是由于as版本,需将Android studio的instant run关闭。具体如下: Settings → Build, Execution, Deployment → Instant Run and uncheck Enable Instant Run.
size_t的全称应该是size type,就是说“一种用来记录大小的数据类型”。属于C99标准,它所定义的变量可以进行加减乘除运算。因此函数中表示数据大小的变量,推荐使用这个类型!例如:
1 | int xxx(voidvoid *p, size_t len); |
以空间换时间。
一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的
数组在作为函数参数传递时会退化为指针:
A declaration of a parameter as “array of type” shall be adjusted to “qualified pointer to type”.
以及前面已经提到的:
int x[3][5];Here x is a 3 × 5 array of integers. When x appears in an expression, it is converted to a pointer to (the first of three) five-membered arrays of integers.
这意味着数组作为参数传递时会丢失边界(C/C++的原生数组本来也就没有边界检查…)。
1 | void funcA(int x[10]){} |
其对应的中间代码为:
1 | ; Function Attrs: nounwind uwtable |
如果数组边界的精确数值非常重要,并且希望函数只接收含有特定数量的元素的数组,可以使用引用形参:
1 | void funcC(int (&x)[10]){} |
其中间代码为:
1 | ; Function Attrs: nounwind uwtable |
如果我们使用数组元素个数不等于10的数组传递给funcC,会导致编译错误:
1 | // note: candidate function not viable: no known conversion from 'int [11]' to 'int (&)[10]' for 1st argument. |
也可以使用函数模板参数来指定函数接收参数的数组大小:
1 | template<int arrSize> |
使用时:
1 | int x[12] |
1 | if((unsigned int)4<(unsigned int)(int)-1){ |
if中的那段表达式是为true的(输出yes),而且编译时也不会发出警告。 虽然我们指定了(int)-1,但是当将unsigned int和int比较时会发生隐式转换。即:
The usual arithmetic conversions are performed on operands of arithmetic or enumeration type.
1 | ((unsigned int)4<(unsigned)(int)-1)==true |
Warnings about conversions between signed and unsigned integers are disabled by default in C++ unless -Wsign-conversion is explicitly enabled.
通过启用-Wsign-conversion就可以看到警告了(建议开启)。 该参数的作用为:
Warn for implicit conversions that may change the sign of an integer value, like assigning a signed integer expression to an unsigned integer variable. An explicit cast silences the warning. In C, this option is enabled also by -Wconversion.
assert Defined in header(c++)/(C)
If NDEBUG is defined as a macro name at the point in the source code where <assert.h> is included, then assert does nothing. If NDEBUG is not defined, then assert checks if its argument (which must have scalar type) compares equal to zero.
1 | #ifdef NDEBUG |
assert只在Debug模式中有效,使用release模assert什么都不做了。 因为在VC++里面,release会在全局定义NDEBUG 下面的代码在VS中使用debug和release模式分别编译并输入>100的数,会有不一样的结果(release不会)
1 | #include <iostream> |
通常情况下我们创建的引用就是有效的,但是也可以人为因素使坏…
1 | char* ident(char *p) { return p; } |
这是UB的行为。
in particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer,which causes undefined behavior.
1 | void f(int(&r)[4]){ |
对于数组引用类型的从参数来说,元素个数也是其类型的一部分。通常只有在模板中才会使用数组引用,此时数组的引用可以通过推断得到。
1 | template<class T,int N> |
这么做的后果是调用f()所用的不同类型的数组有多少个,对应定义的函数有多少个。
为了与C语言兼容,在C++中会自动忽略参数类型的顶层const。
例如下面的函数在C++会报重定义错误,而不是重载:
1 | // 类型是int(int) |
不论对于哪种情况,允许修改实参也好,不允许修改实参也好,它都只是函数调用者提供的实参的一个副本。因此调用过程不会破坏调用上下文的数据安全性。
当char类型用作数组下标时,一定要先转unsigned char(因为char通常是有符号的(依赖实现定义))。不能直接转int或unsigned int,会数组下标越界。
1 | #include <stdio.h> |
struct tag (*[5])(float)
The type designated as struct tag (*[5])(float)
has type ‘‘array of pointer to function returning struct tag’’. The array has length five and the function has a single parameter of type float. Its type category is array.
1 | int TEN=10; |
1 | int ival=0; |
其实我有一个简单的区分的方法:看const修饰的右边是什么。
int const *x=std::nullput;
,const修饰的是x,因为x是指针,我们就暂且把此处的x当做解引用来看,他就代表x所指向的对象,则它就是底层const。int * const x=std::nullptr;
,因为const修饰的是指针x,所以它就是顶层const。如果我们在构造函数中将this指针传递给其它的函数,有可能会引发这样的问题:
1 | struct C; |
看起来上面的代码似乎没什么问题,但是我们构造一个const C的时候,有可能会出现这样的问题:
1 | const C cobj; |
上面的代码会编译通过并可以在no_opt中修改常量对象cobj的成员i的值。 在一个常量对象构造的时候将其this指针传递给其他函数,这意味着我们可以修改该常量中的对象的值,这是不合乎标准的。
During the construction of a const object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor’s this pointer, the value of the object or subobject thus obtained is unspecified.
所以还是不要在构造函数中写将this指针传递出类外的东西(最好还是只初始化数据成员吧)…
有两种方法:
1 | #include <direct.h> |
这种方法有一个弊端:如果将可执行程序添加至系统的PATH路径,则获取到的是在某个目录执行时该目录的路径。
另一种方法是通过Windows API来获取:
1 | const string getTheProgramAbsPath(void){ |
在此种方式下不论是否将该程序添加至系统的PATH路径以及在何处执行,都会获取该可执行程序在系统中存放的绝对路径。
1 | using foofunc=void(int); |
上面的代码里:
1 | foofunc foo; |
是声明一个函数foo,可以看一下目标文件中的符号信息(省去无关细节):
1 | $ clang++ -c testusing.cc -o testusing.o -std=c++11 |
通过gcc工具链中的c++filt可以还原目标文件中的符号:
1 | $ c++filt _Z3fooi |
但是并没有定义,直接链接会产生未定义错误。
1 | int x=123; |
其IR代码为:
1 | # 使用值123初始化x |
从而实现非拷贝行为,其行为类似于将一个对象的地址赋值给一个指针。
其实右值引用的作用就是给临时对象续命——将引用绑定到一个临时对象,不会带来额外的拷贝操作。
实现同样续命行为的还有const T&
:
1 | int x=123; |
和上面的示例在LLVM下会产生一模一样的IR代码。
1 | int a[]={1,2,3,4,5}; |
大多数人都觉得在C++函数中有以下三种传参方式:
Unless a formal argument(parameter) is a reference, a copy of the actual argument is passed to the function.
传指针(by value)只是一种利用指针的性质来实现防止拷贝带来开销的一种技巧,而不是一种传参方式。
如果一个类需要自定义的拷贝构造函数、拷贝赋值操作符、析构函数中的任何一个,那么他往往同时需要三者。
因为编译器生成的隐式定义的copy constructor和operator=语义是逐成员拷贝(memberwise)的,所以如果编译器生成的操作不能够满足类的拷贝需求(比如类成员是具有管理某种资源的句柄),使用编译器的隐式定义会具有浅拷贝,导致两个对象进入某种共享状态。
1 | struct A{ |
如果使用编译器生成的语义会使对象x和y内部共享一块内存,所以需要用户自己定义拷贝构造和拷贝赋值操作符,同样的原因,因为类成员持有某种资源,也需要用户自定义一个析构函数。
C++标准中是这么解释引用的:
[ISO/IEC 14882:2014 §8.3.2]A reference can be thought of as a name of an object.
但是标准中并没有要求应该如何实现引用这一行为(这一点标准中比比皆是),不过多数编译器底层都是使用指针来实现的。 看下列代码:
1 | int a=123; |
然后将其编译为LLVM-IR来看编译器的实际行为:
1 | %2 = alloca i32, align 4 |
可以看到,指针和引用在经过编译器之后具有了完全相同的行为。
在特殊成员函数的隐式声明及其标准行为中提到了编译器会隐式生成和定义六种特殊的成员函数的行为。 因为编译器生成的copy constructor和copy assigment operator均是具有memberwise行为的。所以当我们撰写的类使用浅拷贝可以满足的时候(值语义),没必要自己费劲再写相关的操作了,因为编译器生成的和你手写的一样好,而且不容易出错。
1 | struct A{ |
虽然当你没有显式定义一个copy constructor和copy assignment operator的时候编译器就会隐式定义,但是最好还是自己手动使用=delete指定。 编译器生成的和下面这样手写的一样:
1 | struct A{ |
显然自己手写容易出错,这样的行为可以放心地交给编译器来做。
摘取自《C++编程规范:101条规则/准则与最佳实践》第82条。
1 | vector<int> x{1,2,3,4,5,6,7}; |
STL中的std::remove算法并不真正地从容器中删除元素。因为std::remove属于algorithm,只操作迭代器范围,不掉用容器的成员函数,所以是不可能从容器中真正删除元素的。 来看一下SGISTL中的实现(SGISTL的实现太老,没有用到std::move):
1 | template <class _InputIter, class _Tp> |
可以看到它们只是移动元素的位置,并非真正地把元素删除,只是将不该删除的元素移动到容器的首部,然后返回新的结束位置迭代器。 等于是把删除的部分移动到了元素的尾部,所以要真正地删除容器中所有匹配的元素,需要用erase-remove惯用法:
1 | c.erase(std::remove(c.begin(),c.end(),value),c.end()); // 删除std::remove之后容器尾部的元素 |
如果基类中具有一个虚函数func但是其又重载了几个非虚函数:
1 | struct A{ |
如果我们想要在B对象中使用非虚版本的func函数:
1 | B x; |
这是由于派生类在覆盖基类虚函数的时候会隐藏其他的重载函数,需要在B中显式引入:
1 | struct B:public A{ |
宏在预处理阶段被替换,此时C++的语法和语义规则还没有生效,宏能做的只是简单的文本替换,是极其生硬的工具。 C++中几乎从不需要宏。可以用const和enum定义易于理解的常量。用inline来避免函数调用的开销,用template指定函数系列和类型系列,用namespace避免名字冲突。 除非在条件编译时使用,其他任何时候都没有在C++中使用宏的正当理由。
C++中类内的内存分配函数都是static成员函数:
Any allocation function for a class T is a static member (even if not explicitly declared static).
这意味着operator new/operator delete以及operator new[]/operator delete[]都被隐式声明为static成员函数。
如果我们具有一个类A,其中具有重载的成员函数func,而他们的区别只是该成员函数是否为const,那么在定义一个指向成员函数的指针时如何分别?
1 | struct A{ |
如果我们只是创建一个A::func的指针,指向的只是non-const版本。
1 | void(A::*funcP)()=&A::func; |
想要指定const的版本,就需要在声明时指定const:
1 | void(A::*funcConstP)()const=&A::func; |
对于const的A对象要使用const的版本,对于non-const的A对象要使用non-const的版本,不能混用。
1 | const A x; |
不同于C语言中的宏,使用C++中的模板(template)和谓词(Predicates)可以很轻易的写出泛型的比较操作。 在宏定义中还要注意参数的副作用,因为宏只是简单的替换,比如:
1 | #define MAX(a,b) a>=b?a:b; |
但是这个宏的实际操作这并不是我们所期待的行为。 幸运的是,在C++中我们可以使用模板来避免这种丑陋的宏定义,而且也可以传递一个自定义的谓词来实现我们的判断行为:
1 | struct Compare{ |
在某些情况下,可以通过创建构造函数的方式来提高成员函数的执行效率。
1 | struct String{ |
怎么定义一个类的成员中能够获取到当前类类型的成员呢? 可以用下面这种写法:
1 | template<typename T> |
虽然有种强行搞事的意思…
std::vector可以随机访问,因为其重载了[]操作符,以及有at成员函数,则通常有下面两种方式:
1 | template<typename T> |
以上两种随机访问方式有什么区别?
顺序容器的at(size_type)要求有范围检查。 [ISO/IEC 14882:2014]The member function at() provides bounds-checked access to container elements. at() throws out_of_range if n >= a.size(). 而operator[]标准中则没有任何要求。
可以来看一下一些STL实现(SGISTL)的源码对std::vector的operator[size_type]和at(size_type)的实现: 首先是at(size_type)的实现
1 | // at(size_type)的实现 |
再看一下operator[] (size_type)的实现:
1 | // operator[](size_type)的实现 |
可以看到,operator[]的随机访问并没有范围检查。 即上面的问题:
1 | x[0]; |
这两个的区别在于,若x不为空,则行为相同,若x为空,x.at(0)则抛出一个std::out_of_range异常(C++标准规定),而x[0]是未定义行为。
1 | typedef int* INTPTR; |
还是直接从IR代码来看吧:
1 | %6 = alloca i32*, align 8 |
注意%9
不是i32*
,它是一个i32的对象。
因为#define
只是编译期的简单替换,所以在编译期展开的时候会变成这样:
1 | #define INTPTR2 int* |
即只有i3为int*
,而i4则为int
1 | const int x=10; |
这里是可以的,在编译器优化下x会直接被替换为10 其中间代码如下:
1 | %6 = alloca i32, align 4 |
可以看到%7
的分配时并没有使用%6,所以也并不依赖x这个对象,这个对象是编译期已知的。
但是,当我们这么写时,又如何编译期可知:
1 | int x; |
这里是由于编译器扩展,所以C++也支持VLA。但是可以看到const是没办法为编译期常量的。
在类的继承层次中,可能具有同一基类的几个不同的派生类,他们之间可能又互相继承派生出了几个继承层次,在这样的情况下如何判断某一个派生类的层次中是否继承自某一个类呢?
可以使用dynamic_cast来实现我们的要求,关于C++类型转换的部分可以看我之前的一篇文章:详细分析下C++中的类型转换。下面先来看一下dynamic_cast在C++标准中的描述(ISO/IEC 14882:2014):
The result of the expression dynamic_cast
(v) is the result of converting the expression v to type T. T shall be a pointer or reference to a complete class type, or “pointer to cv void.” The dynamic_cast operator shall not cast away constness (5.2.11).
If C is the class type to which T points or refers, the run-time check logically executes as follows:
The value of a failed cast to pointer type is the null pointer value of the required result type. A failed cast to reference type throws an exception (15.1) of a type that would match a handler (15.3) of type std::bad_cast (18.7.2).
所以我们可以对继承层次中的类指针执行dynamic_cast转换,检查是否转换成功,从而判断继承层次中是否具有某个类。 一个代码的例子如下:
1 | #include <iostream> |
面的继承层次比较简单,但是当假设我们不知道Cricle和Square的具体继承层次时,那么如何判断Square中是否存在某一基类(如Roll)? 解决的办法就是上面提到的dynamic_cast!通过dynamic_cast转换到转换到要检测的类类型的指针,如果转换成功,dynamic_cast会返回从源类型转换到目标类型的指针,如果失败会返回一个空指针(之所以不使用引用是因为要处理可能会抛出异常的潜在威胁),这种转换并非是向上或者向下转型,而是横向转型。所以我们需要对dynamic_cast返回的对象(指针)作一个判断就可以得出检测目标的继承层次中是否存在要检测的类型。
但是,我觉得这种行为的适用场景十分狭窄,在良好的类设计下几乎不必要,如果你对自己所实现的类层次感到失控,那一定是糟糕的设计。
Segmetation fault也叫做段错误,引发的原因有好多,这里我们只说一下段错误发生时的调试方法。
这是最基本的往往也很有效的方法,在哪里Core掉就会在哪里停止打印–一目了然。同时这种方法也存在一个致命缺陷:如果恰巧Core掉的地方没加打印而程序代码又非常庞大又可能是多线程的,那查找问题等同于大海捞针。
加gdb调试往往能在Core dump时抓到,甚至能抓到哪一个文件哪个类哪个函数哪一行,甚是精确。要确保GDB能抓到可用信息要做一些准备:
有了上面两点对大多数的Segmentation fault都能抓住,但是函数调用栈彻底乱掉或者在动态库so中Core而这个库编译时没有加-g参数,这些情况就gdb就无能为力了。
这种方法其实是借住两个系统函数backtrace和backtrace_symbol来获取函数调用栈的,把这两个函数放在信号处理函数中:当收到 SIGSEG时在信号处理函数中调用这两个函数打印函数调用栈,在没用GDB调试的时候这种方法可以代替gdb的一部分功能,这听起来是不是非常酷啊,来看一看实现吧:
1 | #include <signal.h> |
当然这种方法在没有GDB时候会大显身手,经过实验就是有gdb的时候这种方法有时比gdb抓到调用栈要多一层;当然这种方法和用gdb调试一样要加-g
和栈保护参数-fstack-protector
和 -fstack-protector-all
。其缺点就是抓到的调用栈无效,这是什么意思呢?有时发生core dump,能定位到甚至哪一行,但是那一行根本没有明显的错误;或者追到没有调试信息的动态库里如glibc。当然这些情况大多数调试方法都无能为力,只能依靠程序员的经验了。
如果我们的程序是多线程的,发生core dump用以上方法均无效,除了仔细排查代码外,还有这么一方法让我们缩小范围。
一般一个稍大的linux项目会有很多个源文件组成,最终的可执行程序也是由这许多个源文件编译链接而成的。编译是把一个.c或.cpp文件编译成中间代码.o文件,链接是就使用这些中间代码文件生成可执行文件。比如在当前项目目录下有如下源文件:
1 | # ls |
以上源代码可以这样编译:
1 | # gcc -o target_bin main.c debug.c ipc.c timer.c tools.c |
如果之后修改了其中某一个文件(如tools.c),再执行一下上一行代码即可,但如果有成千上万个源文件这样编译肯定是不够合理的。此时我们可以按下面步骤来编译:
1 | # gcc -c debug.c |
如果其中tools.c修改了,只需要编译该文件,再执行最后生成可执行文件的操作,也就是做如下两步操作即可:
1 | # gcc -c tools.c |
这样做看上去应该很合理了。但是如果修改了多个文件,就很可能忘了编译某一文件,那么运行时就很有可能出错。如果是common.h文件修改了,那么包含该头文件的所有.c文件都需要重新编译,这样一来的话就更复杂更容易出错了。看来这种方法也不够好,手动处理很容易出错。那有没有一种自动化的处理方式呢?有的,那就是写一个Makefile来处理编译过程。 下面给一个简单的Makefile,在源代码目录下建一个名为Makefile的文件:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
然后在命令行上执行命令:
1 | # make |
可见在该目录下生成了.o文件以及target_bin可执行文件。现在我们只需要执行一个make命令就可以完成所有编译工作,无需像之前一样手动执行所有动作,make命令会读取当前目录下的Makefile文件然后完成编译步骤。从编译过程输出到屏幕的内容看得到执行make命令之后所做的工作,其实就是我们之前手动执行的那些命令。现在来说一下什么是Makefile? 所谓Makefile我的理解其实就是由一组组编译规则组成的文件,每条规则格式大致为:
1 | target ... : prerequisites ... |
其中target是目标文件,可以为可执行文件、*.o
文件或标签。Prerequisites是产生target所需要的源文件或*.o
文件,可以是另一条规则的目标。commond是要产生该目标需要执行的操作系统命令,该命令必须以tab(文中以>—标示tab字符)开头,不可用空格代替。
说白了就是要产生target,需要依赖后面的prerequisites文件,然后执行commond来产生来得到target。这和我们之前手动执行每条编译命令是一样的,其实就是定义好一个依赖关系,我们把产生每个文件的依赖文件写好,最终自动执行编译命令。
比如在我们给出的Makefile例子中target_bin main.o等就是target,main.o debug.o ipc.o timer.o tools.o是target_bin的prerequisites,gcc -o target_bin main.o debug.o ipc.o timer.o tools.o就是commond,把所有的目标文件编译为最终的可执行文件target,而main.c common.h是main.o的prerequisites,其gcc -c main.c命令生成target所需要的main.o文件。
在该例子中,Makefile工作过程如下:
在没有更改源代码的情况下,再次运行make:
1 | # make |
得到提示目标target_bin已经是最新的了。 如果修改文件main.c之后,再运行make:
1 | # vim main.c |
此时make会自动选择受影响的目标重新编译: 首先更新缺省目标,先检查target_bin是否需要更新,这需要检查其依赖文件main.o debug.o ipc.o timer.o tools.o是否需要更新。 其次发现main.o需要更新,因为main.o目标的依赖文件main.c最后修改时间比main.o晚,所以需要执行生成目标main.o的命令:gcc -c main.c更新main.o。 最后发现目标target_bin的依赖文件main.o有更新过,所以执行相应命令gcc -o target_bin main.o debug.o ipc.o timer.o tools.o更新target_bin。 总结下,执行一条规则步骤如下:
Makefile中有很多目标,我们可以编译其中一个指定目标,只需要在make命令后面带上目标名称即可。如果不指定编译目标的话make会编译缺省的目标,也就是第一个目标,在本文给出的Makefile第一个目标为target_bin。如果只修改了tools.c文件的话,我们可能只想看看我们的更改的源代码是否有语法错误而又不想重新编译这个工程的话可以执行如下命令:
1 | # make tools.o |
编译成功,这里又引出一个问题,如果继续执行同样的命令:
1 | # make tools.o |
我们先手动删掉tools.o文件再执行就可以了,怎么又是手动呢?我们要自动,要自动!!好吧,我们加一个目标来删除这些编译过程中产生的临时文件,该目标为clean。 我们在上面Makefile最后加上如下内容:
1 | clean: |
当我们直接make命令时不会执行到该目标,因为没有被默认目标target_bin目标或以target_bin依赖文件为目标的目标包含在内。我们要执行该目标需要在make时指定目标即可。如下:
1 | # make clean |
可见clean目标被执行到了,再执行make时make就会重新生成所有目标对应的文件,因为执行make clean时,那些文件被清除了。 clean目标应该存在与你的Makefile当中,它既可以方便你的二次编译,又可以保持的源文件的干净。该目标一般放在最后,不可放在最开头,否则会被当做缺省目标被执行,这很可能不是你的意愿。 最后总结一下,Makefile只是告诉了make命令如何来编译和链接程序,告诉make命令生成目标文件需要的文件,具体的编译链接工作是你的目标对应的命令在做。 给一个今天完整的makefile:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
上一节的Makefile勉强可用,但还写的比较繁琐,不够简洁。对每一个.c源文件,都需要写一个生成其对应的.o目标文件的规则,如果有几百个或上千个源文件,都手动来写,还不是很麻烦,这也不够自动化啊。 这样,我们把生成.o目标文件的规则全部删除掉,就是这样一个Makefile文件:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
这下简洁了不少,这样也能用吗?试试看吧先,make一下:
1 | # make |
原来酱紫都可以啊!!target_bin后面那一群依赖文件怎么生成呢?不是没有生成*.o目标文件的规则了吗?再看屏幕编译输出内容:
1 | cc -c -o main.o main.c |
怎么长的和之前不太一样呢,尤其是前面那个cc是何物? 其实make可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个*.o文件后都写上类似的命令,因为,我们的 make 会自动推导依赖文件,并根据隐含规则自己推导命令。所以上面.o文件是由于make自动推导出的依赖文件以及命令来生成的。 下面来看看make是如何推导的。 命令make –p可以打印出很多默认变量和隐含规则。Makefile变量可以理解为C语言的宏,直接展开即可(后面会讲到)。取出我们关心的部分:
1 | # default |
其中cc是一个符号链接,指向gcc,这就可以解释为什么我们看到的编译输出为cc,其实还是使用gcc在编译。
1 | # ll /usr/bin/cc |
变量$(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH)
都为空。所以%.o: %.c
规则命令展开为:
1 | cc -c -o $@ $< |
再看屏幕输出编译内容,摘取一条:
1 | cc -c -o main.o main.c |
不是看出点什么?$@
和main.o对应,$<
和main.c对应。其实$@
和$<
是两个变量。$@
为规则中的目标,$<
为规则中的第一个依赖文件。%.o:%.c
是一种称为模式规则的特殊规则。因为main.o符合该模模式,再推导出依赖文件main.c,最终推导出整个规则为:
1 | main.o : main.c: |
其余几个目标也同样推导。make自动推导的功能为我们减少了不少的Makefile代码,尤其是对源文件比较多的大型工程,我们的Makefile可以不用写得那么繁琐了。 最后,今天的Makefile相对于上一节进化成这个样子了:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
仔细研究我们的之前Makefile发现,我们还有改进的地方,就是此处:
1 | target_bin : main.o debug.o ipc.o timer.o tools.o |
如果增加一个源文件xx.c的话,需要在两处或多处增加xx.o文件。我们可以使用变量来解决这个问题。之前说过,Makefile的变量就像C语言的宏一样,使用时在其位置上直接展开。变量在声明时赋予初值,在引用变量时需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。 默认目标target_bin也在多处出现了,该文件也可以使用变量代替。 修改我们的Makefile如下:
1 | SRC_OBJ = main.o debug.o ipc.o timer.o tools.o |
这样每次有新增的文件是只需要在SRC_OBJ变量里面增加一个文件即可。要修改最终目标的名字是可以只修改变量SRC_BIN。 其实在之前还说过特殊变量:
$@
,表示规则中的目标。$<
,表示规则中的第一个依赖文件。$?
,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
$^
:,表示规则中的所有条件,组成一个列表,以空格分隔。
上一节我们看到make -p有很多自定义的变量,比如CC。其中很多变量我们可以直接使用或修改其变量值或增加值。我们的Makefile中可以使用CC(默认值为cc)、RM(默认值为rm -f)。由此可见我们的Makefile还可以进一步修改:
1 | SRC_OBJ = main.o debug.o ipc.o timer.o tools.o |
这样的Makefile编译也是可用的。
但是这样的Makefile还是需要我们手动添加文件,还是不够自动化,最好增删文件都要修改Makefile。伟大的人类真是太懒了!!于是乎,他们发明了一个函数wilcard
(函数后面会讲到),它可以用来获取指定目录下的所有的.c文件列表。这样的话我们可以自动获取当前目录下所有.c源文件,然后通过其他方法再得到.o文件列表,这样的话就不需要在每次增删文件时去修改Makefile了。所谓其他方法这里给出两种:
$(patsubst %.c,%.o,$(dir) )
中,patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o。“$(var:a=b)”
或“${var:a=b}”
,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。修改后的Makefile如下:
1 | # SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c)) |
其中# 后面的内容为注释。 这样终于满足了那些懒人的想法了。可见在使用变量时,的确可以是编译变得更自动化。
其实变量的定义有三种运算符=、:=、?=、+=
。
=
运算符可以读取到后面定义的变量。比如:1 | VAR = $(VAR2) |
运行结果为:
1 | # |
但是这种定义可能会导致并非我们意愿的事发生,并不是很符合C语言的编程习惯。
:=
运算符在遇到变量定义时立即展开。1 | VAR := $(VAR2) |
运行结果为:
1 | # |
?=
运算符在复制之前先做判断变量是否已经存在。例如var1 ?= $(var2)
的意思是:如果var1没有定义过,那么?=
相当于=
,如果var1先前已经定义了,则什么也不做,不会给var重新赋值。+=
运算符是给变了追加值。如果变量还没有定义过就直接用+=赋值,那么+=
相当于=
如何使用这几个运算符要看实际情况,有时一个大的工程可能有许多Makefile组成,变量可能在多个Makefile中都在使用,这时可能使用+=
比较好。使用:=
有时可能比要好。
有时在编译程序时,我们需要编译器给出警告,或加入调试信息,或告知编译器优化可执行文件。编译时C编译器的选项CFLAGS使用的较多,默认没有提供值,我们可以给该变量赋值。有时我们还需要使用链接器选项LFLAGS告诉链接器链接时需要的库文件。可能我们还需要给出包含头文件的路径,因为头文件很可能和源文件不再同一目录。所以,我们今天的Makefile加上部分注释又更新了:
1 | # A commonMakefile for c programs, version 1.0 |
编译:
1 | # make |
可见我们的预编译选项,编译选项都用到了,之前我们说过make的使用隐含规则自动推导:
1 | COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) –c |
其中变量CFLAGS 和 CPPFLAGS均是我们给出的,变量$(TARGET_ARCH)未给,所以在编译输出可以看到-c前面有2个空,最早未给变量是有四个空。 目前给出的Makefile基本上可以适用于那些源代码全部在同一目录下的简单项目,并且基本上在增删文件时不需要再去手动修改Makefile代码。在新的一个项目只需要把该Makefile拷贝到源代码目录下,再修改一下你需要编译的可执行文件名称以及你需要的编译连接选项即可。 后面章节将会讲到如何写多目录源代码工程下的Makefile。 最后,今天的最终Makefile是这样的:
1 | # A commonMakefile for c programs, version 1.0 |
一般情况下,Makefile都会有一个clean目标,用于清除编译过程中产生的二进制文件。我们在第一节的Makefile就用到了这个 clean目标,该目标没有任何依赖文件,并且该目标对应的命令执行后不会生产clean文件。 像这种特点目标,它的规则所定义的命令不是去创建文件,而仅仅通过make指定目标来执行一些特定系统命令或其依赖为目标的规则(如all),称为伪目标。 一个Makefile一般都不会只有一个伪目标,如果按Makefile的“潜规则”以及其约定俗成的名字来说的话,在较大的项目的Makefile中比较常用的为目标有这些:
make处理Makefile时,首先读取所有规则,建立关系依赖图。然后从缺省目标(第一个目标)或指定的目标开始执行。像clean,tags这样的目标一般不会作为缺省目标,也不会跟缺省目标有任何依赖关系,所以 make 无法生成它的依赖关系和决定它是否要执行。所以要执行这样的目标时,必须要显示的指定make该目标。就像前面我们清楚便已产生的中间二进制文件一样,需要显示执行命令:make clean。 伪目标也可以作为默认目标(如all),并且可以为其指定依赖文件。 我们先将version 1.0的Makefile完善下,我们可以加入帮助信息,tags等功能。
1 | # A common Makefile for c programs, version 1.1 |
make会把执行的命令打印在屏幕上,如果我们不想把命令打印在屏幕上,只显示命令结果时,直接在命令前面加上符号“@”就可以实现。如上面help目标一样,只显示命令结果。一般我们会在make时都会输出“Compiling xxx.c…”,不输出编译时的命令。我们在后面写Makefile时可以模仿。 如果当前目录下存在一个和伪目标同名的文件时(如clean),此时如果执行命令make clean后出现如下结果:
1 | # touch clean |
这是因为clean文件没有依赖文件,make认为目标clean是最新的不会去执行规则对应的命令。为了解决这个问题,我们可以明确地将该目标声明为伪目标。将一个目标声明为伪目标需要将它作为特殊目标.PHONY”的依赖。如下:
1 | .PHONY : clean |
这条规则写在clean:规则的后面也行,也能起到声明clean是伪目标的作用 这样修改一下之前Makefile,将所有伪目标都作为.PHONY的依赖:
1 | .PHONY : all obj tag help clean disclean |
这样在当前目录下存在文件clean时执行:
1 | # make clean |
发现问题解决。 最后,给出今天最终的Makefile:
1 | # A common Makefile for c programs, version 1.1 |
在大一些的项目里面,所有源代码不会只放在同一个目录,一般各个功能模块的源代码都是分开的,各自放在各自目录下,并且头文件和.c源文件也会有各自的目录,这样便于项目代码的维护。这样我们可以在每个功能模块目录下都写一个Makefile,各自Makefile处理各自功能的编译链接工作,这样我们就不必把所有功能的编译链接都放在同一个Makefile里面,这可使得我们的Makefile变得更加简洁,并且编译的时候可选择编译哪一个模块,这对分块编译有很大的好处。 现在我所处于工程目录树如下:
1 | . |
这样组织项目源码要比之前合理一些,那这样怎么来写Makefile呢?我们可以在每个目录下写一个Makefile,通过最顶层的Makefile一层一层的向下嵌套执行各层Makefile。那么我们最顶层的Makefile简单点的话可以这样写:
1 | # top Makefile for xxx |
命令:
1 | >---$(MAKE) -C src |
就是进入src目录继续执行该目录下的Makefile。然后src目录下的Makefile在使用同样的方法进入下一级目录tools、main、ipc,再执行该目录下的Makefile。其实这样有些麻烦,我们可以直接从顶层目录进入最后的目录执行make。再加入一些伪目标完善下,我们的顶层Makefile就出来了:
1 | # Top Makefile for C program |
当我们这样组织源代码时,最下面层次的Makefile怎么写呢?肯定不可以将我们上一节的Makefile(version 1.1)直接拷贝到功能模块目录下,需要稍作修改。不能所有的模块都最终生成各自的可执行文件吧,我们目前是一个工程,所以最后只会生成一个可执行程序。我们这样做,让主模块目录生成可执行文件,其他模块目录生成静态库文件,主模块链接时要用其他模块编译产生的库文件来生成最终的程序。将上一节Makefile稍作修改得出编译库文件Makefile和编译可执行文件Makefile分别如下:
1 | # A Makefile to generate archive file |
====================
1 | # A Makefile to generate executive file |
最后在顶层执行:
1 | # make clean |
最后生成了可执行程序文件。这样的话一个工程的各个模块就变得独立出来了,不但源码分开了,而且各自有各自的Makefile,并且各个功能模块是可独立编译的。 我们发现顶层Makefile还有可以改进的地方,就是在进入下一层目录是要重复写多次,如下:
1 | >---$(MAKE) -C src/ipc |
每增加一个目录都要在多个伪目标里面加入一行,这样不够自动化啊,于是我们想到shell的循环语 句,我们可以在每条规则的命令处使用for循环。如下:
1 | DIR = src |
这样懒人有可以高兴很久了。不过还有问题: 上面for循环会依次进入系统命令ls列出的目录,但我们对每个目录的make顺序可能有要求,在该项目当中,main目录下的Makefile必须最后执行,因为最终的链接需要其他目录编译生成的库文件,否则会执行失败。并且在当前的Makefile中,当子目录执行make出现错误时,make不会退出。在最终执行失败的情况下,我们很难根据错误的提示定位出具体是是那个目录下的Makefile出现错误。这给问题定位造成了很大的困难。为了避免这样的问题,在命令执行错误后make退出。 所以将刚才的Makefile修改为如下
1 | DIR = src |
这样在执行出错时立马退出,但这样还是没有解决问题,编译错误还是会出现。那怎么解决呢? 我们可以通过增加规则来限制make执行顺序,这样就要用到伪目标,对每一个模块我们都为他写一条规则,每个模块名称是目标,最后需要执行的模块目标又是其他模块的目标,这样就限制了make顺序。在执行到最后需要执行的目标时,发现存在依赖,于是先更新依赖的目标,这样就不会出错了。并且这样的话,我们还可以对指定模块进行编译,比如我只修改了tools模块,我只想看看我修改的这个模块代码是否可以编译通过,我可以在编译时这样:
1 | # make tools |
还有另外一种方法也可以解决此问题,就是手动列出需要进入执行的模块名称(这里就是目录了),把最后需要执行的模块放在最后,这样for循环执行时最后需要编译链接的模块就放在最后了,不会像我们之前那样make是按照使用系统命令ls列出模块目录的顺序来执行。ls列出目录是按照每个目录的名称来排序的,我们总不能要求写代码的时候最后执行的模块的名称必须是以z开头的吧,总之不现实。
我们的顶层Makefile又进化了,也是这一节最终Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49# Top Makefile for C program
# Copyright (C) 2014 shallnew \at 163 \dot com
DIR = src
MODULES = $(shell ls $(DIR))
# MODULES = ipc main tools
all : $(MODULES)
$(MODULES):
>---$(MAKE) -C $(DIR)/$@
main:tools ipc
obj:
>---@for subdir in $(MODULES); \
>---do $(MAKE) -C $(DIR)/$$subdir $@; \
>---done
clean :
>---@for subdir in $(MODULES); \
>---do $(MAKE) -C $(DIR)/$$subdir $@; \
>---done
distclean:
>---@for subdir in $(MODULES); \
>---do $(MAKE) -C $(DIR)/$$subdir $@; \
>---done
tags:
>---ctags -R
help:
>---@echo "===============A common Makefilefor c programs=============="
>---@echo "Copyright (C) 2014 liuy0711 \at 163\dot com"
>---@echo "The following targets aresupport:"
>---@echo
>---@echo " all - (==make) compile and link"
>---@echo " obj - just compile, withoutlink"
>---@echo " clean - clean target"
>---@echo " distclean - clean target and otherinformation"
>---@echo " tags - create ctags for vimeditor"
>---@echo " help - print help information"
>---@echo
>---@echo "To make a target, do 'make[target]'"
>---@echo "========================= Version2.0 ======================="
.PHONY : all clean distclean tags help
在多个Makefile嵌套调用时,有时我们需要传递一些参数给下一层Makefile。比如我们在顶层Makefile里面定义的打开调试信息变量DEBUG_SYMBOLS,我们希望在进入子目录执行子Makefile时该变量仍然有效,这是需要将该变量传递给子Makefile,那怎么传递呢?这里有两种方法:
在上层Makefile中使用”export”关键字对需要传递的变量进行声明。比如:
1 | DEBUG_SYMBOLS = TRUE |
当不希望将一个变量传递给子 make 时,可以使用指示符 “unexport”来声明这个变量。 export一般用法是在定义变量的同时对它进行声明。如下:
1 | export DEBUG_SYMBOLS = TRUE |
1 | $(MAKE) -C xxx DEBUG_SYMBOLS = TRUE |
这样在进入子目录xxx执行make时该变量也有效。
像编程语言一样,Makefile也有自己的条件语句。条件语句可以根据一个变量值来控制make的执行逻辑。比较常用的条件语句是ifeq –else-endif、ifneq-else-endif、ifdef-else-endif。 ifeq关键字用来判断参数是否相等。 比如判断是否生成调试信息可以这么用:
1 | ifeq ($(DEBUG_SYMBOLS), TRUE) |
Ifneq和ifeq作用相反,此关键字是用来判断参数是否不相等。 ifdef关键字用来判断一个变量是否已经定义。 后两个关键字用法和ifeq类似。
现在我们继续改进我们上一节的Makefile,上一节的Makefile完成Makefile的嵌套调用,每一个模块都有自己的Makefile。其实每个模块的Makefile都大同小异,只需要改改最后编译成生成的目标名称或者编译链接选项,规则都差不多,那么我们是否可以考虑将规则部分提取出来,每个模块只需修改各自变量即可。这样是可行的,我们将规则单独提取出来,写一个Makefile.rule,将他放在顶层Makefile同目录下,其他模块内部的Makefile只需要include该Makefile就可以了。如下:
1 | include $(SRC_BASE)/Makefile.rule |
include类似于C语言的头文件包含,你把它理解为为本替换就什么都明白了。 这样以后规则有修改的话我们直接修改该Makefile就可以了,就不用进入每一个模块去修改,这样也便于维护。 这样我们今天顶层Makefile稍作修改:
1 | # Top Makefile for C program |
目前我们顶层目录下的目录树为:
1 | . |
每个子模块下的Makefile删除规则后修改为如下:
1 | SRC_BASE = ../.. |
而处于顶层目录下的Makefile.rule专门处理各模块编译链接时需要的规则。内容如下:
1 | # Copyright (C) 2014 shallnew \at 163 \dot com |
我们将Makefile.rule放在顶层有可能会一不小心在命令行上面执行了该Makefile,如下:
1 | # make -f Makefile.rule |
由于我们没有定义变量$(SRC_BIN)
和$(SRC_LIB)
,伪目标all没有任何依赖,所以编译是无法成功的。这里我们我们应该禁止直接执行该Makefile。
在make里面有这样一个变量:MAKELEVEL,它在多级调用的 make 执行过程中。变量代表了调用的深度。在 make 一级级的执行过程中变量MAKELEVEL的值不断的发生变化,通过它的值我们可以了解当前make 递归调用的深度。顶层的MAKELEVEL的值为“0” 、下一级时为“1” 、再下一级为“2”…….,所以我们希望一个子目录的Makefile必须被上层 make 调用才可以执行,而不允许直接执行,我们可以判断变量MAKELEVEL来控制。所以我们这一节最终的Makefile.rule为:
1 | # Copyright (C)2014 shallnew \at 163 \dot com |
此时再直接执行该Makefile:
1 | # make -f Makefile.rule |
上一节我们把规则单独提取出来,方便了Makefile的维护,每个模块只需要给出关于自己的一些变量,然后再使用统一的规则Makefile。这一节我们继续改进我们的Makefile,到目前为止我们的Makefile编译链接输出的目标都在源文件同目录下或模块Makefile同一目录下,当一个项目大了之后,这样会显得很乱,寻找编译输出的文件也比较困难。既然Makefile本身就是按照我们的的规则来编译链接程序,那么我们就可以指定其编译链接目标的目录,这样,我们可以清楚输出文件的地方,并且在清除已编译的目标时直接删除指定目录即可,不需要一层一层的进入源代码目录进行删除,这样又提高了效率。
既然要统一目标输出目录,那么该目录就需要存在,所以我们可以增加一条规则来创建这些目录,包括创建可执行文件的目录、链接库文件的目录以及.o文件的目录。并且目录还可以通过条件判断根据是否产生调试信息来区分开相应的目标文件。一般一个工程的顶层目录下都会有一个build目录来存放编译的目标文件结果,目前我的工程目录下通过Makefile创建的目录build的目录树如下:
1 | build/ //build根目录 |
ifeq ($(DEBUG_SYMBOLS), TRUE)
—BUILDDIR = ./build/$(PLATFORM)_dbg
else
—BUILDDIR = ./build/$(PLATFORM)
endif
all : $(BUILDDIR) $(MODULES)
$(BUILDDIR):
—@echo “ Create directory $@ …”
—mkdir -p $(BUILDDIR)/bin $(BUILDDIR)/lib
1 | 我们在all目标里面增加了其依赖目标BUILDDIR,该目标对应的规则为创建bin目录和lib目录。这样每次编译之前都会创建目录。 |
……
ifeq ($(SRC_BASE),)
—BUILDDIR = $(MOD_SRC_DIR)
—OBJDIR = $(MOD_SRC_DIR)
—LIBDIR = $(MOD_SRC_DIR)
—BINDIR = $(MOD_SRC_DIR)
else
—ifeq ($(DEBUG_SYMBOLS), TRUE)
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)_dbg
—else
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)
—endif
—OBJDIR = $(BUILDDIR)/obj/$(MODULE)
—LIBDIR = $(BUILDDIR)/lib
—BINDIR = $(BUILDDIR)/bin
endif
……
ifeq ($(MAKELEVEL), 0)
all : msg
else
all : lib bin
endif
lib : $(OBJDIR) $(SRC_LIB)
bin : $(OBJDIR) $(SRC_BIN)
$(OBJDIR) :
—@echo “ MKDIR $(notdir $@)…”
—@mkdir -p $@
……
1 | 此时我们编译一下后查看build目录: |
build/
└── unix_dbg
├── bin
├── lib
└── obj
├── ipc
├── main
└── tools
7 directories, 0 files
1 | 由于我们是开启了调试信息,所以创建了unix_dbg目录,并且该目录下创建了bin、lib、obj目录及其模块目录,但我们没有发现有文件存放在里面。 |
lib : $(OBJDIR) $(LIBDIR)/$(SRC_LIB)
bin : $(OBJDIR) $(BINDIR)/$(SRC_BIN)
$(OBJDIR) :
—@echo “ MKDIR $(notdir $@)…”
—@mkdir -p $@
ifneq ($(SRC_BIN),)
$(BINDIR)/$(SRC_BIN) : $(SRC_OBJ)
—$(CC) -o $@ $^ $(LDFLAGS)
endif
ifneq ($(SRC_LIB),)
$(LIBDIR)/$(SRC_LIB) : $(SRC_OBJ)
—$(AR) rcs $@ $^
—cp $@ $(SRC_BASE)/libs
endif
1 | 此时再执行make,完成后查看build目录树: |
build/
└── unix_dbg
├── bin
│ └── target_bin
├── lib
│ ├── libipc.a
│ └── libtools.a
└── obj
├── ipc
├── main
└── tools
1 | 可以看到,生成的目标是在对应目录下。我们乘胜追击,把.o文件也将其修改了。我们之前的每个模块Makefile大致是这样写的: |
SRC_BASE = ../..
CFLAGS +=
CPPFLAGS += -I. -I./inc -I$(SRC_BASE)/include
SRC_FILES = $(wildcard src/*.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_LIB = xx.a
include $(SRC_BASE)/Makefile.rule
1 | 其中SRC_OBJ在此处给出,然后再在Makefile.rule中使用,此处的.o文件会在.c文件相同目录下生成,所以我们现在需要将.o文件加上路径,由于取得路径是在Makefile.rule里面,所以我们可以统一在Makefile.rule里面给变量SRC_OBJ赋值,大致如下: |
SRC_OBJ = $(patsubst %.c, $(OBJDIR)/%.o, $(notdir $(SRC_FILES)))
1 | 这里用到函数patsubst、notdir,关于函数会在后面讲到。这样.o文件作为目标生成之后就会生成到相应目录里面了。 |
make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc'
make[1]: *** No rule to make target
../../build/unix_dbg/obj/ipc/ipc.o’, needed by ../../build/unix_dbg/lib/libipc.a'. Stop.
make[1]: Leaving directory
/home/Myprojects/example_make/version-2.9/src/ipc’
make: *** [ipc] Error 2
1 | 发现出错了,并且是在生成目标文件ipc.o时没有成功,查看build目录树也没有生成.o文件。为什么会生成失败呢? |
%.o: %.c
—$(COMPILE.c) $(OUTPUT_OPTION) $<
1 | 该模式规则中目标文件是$(OBJDIR)/%.o,那么现在有了符合生成我们需要的.o文件的规则了,编译一下: |
make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc'
make[1]: *** No rule to make target
../../build/unix_dbg/obj/ipc/ipc.o’, needed by ../../build/unix_dbg/lib/libipc.a'. Stop.
make[1]: Leaving directory
/home/Myprojects/example_make/version-2.9/src/ipc’
make: *** [ipc] Error 2
#
1 | 发现还是不对,不是已经增加了模式规则了吗,为何还是没有生成.o文件。 |
<targets …>:
….
1 | 比如下面是一个静态模式规则: |
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
1 | 该规则描述了所有的.o文件的依赖文件为对应的.c文件,对于目标“foo.o” ,取其茎“foo”替代对应的依赖模式“%.c”中的模式字符“%”之后可得到目标的依赖文件“foo.c”。这就是目标“foo.o”的依赖关系“foo.o: foo.c”,规则的命令行描述了如何完成由“foo.c”编译生成目标“foo.o” 。命令行中“$<”和“$@”是自动化变量,“$<” 表示规则中的第一个依赖文件, “$@” 表示规则中的目标文件。上边的这个规则描述了以下两个具体的规则: |
foo.o : foo.c
—$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
—$(CC) -c $(CFLAGS) bar.c -o bar.o
1 | 注:该示例与其相关描述摘抄于互联网,描述很不错,估计比我讲的详细) |
$(SRC_OBJ) : $(OBJDIR)/%.o : %.c
—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1 | 执行后: |
make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc'
make[1]: *** No rule to make target
ipc.c’, needed by ../../build/unix_dbg/obj/ipc/ipc.o'. Stop.
make[1]: Leaving directory
/home/Myprojects/example_make/version-2.9/src/ipc’
make: *** [ipc] Error 2
#
1 | 发现提示没有文件ipc.c,这说明没有生成.o的原因是没有.c文件,我很好奇的是为何使用非静态模式为何不提示呢?(还没搞懂,再研究研究,知道的可以给个提示哈~~) |
VPATH += ./src
1 | 指定了依赖搜索目录为当前目录下的src目录,我们可以在Makefile.rules里面添加给VPATH变量赋值,而在包含该Makefile.rules之前给出当前模块.c文件所在目录。 |
$(SRC_OBJ) : $(OBJDIR)/%.o : $(MOD_SRC_DIR)/%.c
—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1 |
|
VPATH += ./src
1 | 可以写为: |
vpath %.c ./src
1 | 现在给一个我们的Makefile.rules: |
ifndef PLATFORM
—PLATFORM = unknow
endif
ifeq ($(SRC_BASE),)
—BUILDDIR = $(MOD_SRC_DIR)
—OBJDIR = $(MOD_SRC_DIR)
—LIBDIR = $(MOD_SRC_DIR)
—BINDIR = $(MOD_SRC_DIR)
else
—ifeq ($(DEBUG_SYMBOLS), TRUE)
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)_dbg
—else
—>—BUILDDIR = $(SRC_BASE)/build/$(PLATFORM)
—endif
—OBJDIR = $(BUILDDIR)/obj/$(MODULE)
—LIBDIR = $(BUILDDIR)/lib
—BINDIR = $(BUILDDIR)/bin
endif
ifeq ($(DEBUG_SYMBOLS), TRUE)
—CFLAGS += -g -Wall -Werror -O0
else
—CFLAGS += -Wall -Werror -O2
endif
VPATH += $(MOD_SRC_DIR)
SRC_OBJ = $(patsubst %.c, $(OBJDIR)/%.o, $(notdir $(SRC_FILES)))
ifeq ($(MAKELEVEL), 0)
all : msg
else
all : lib bin
endif
lib : $(OBJDIR) $(LIBDIR)/$(SRC_LIB)
bin : $(OBJDIR) $(BINDIR)/$(SRC_BIN)
$(OBJDIR) :
—mkdir -p $@
ifneq ($(SRC_BIN),)
$(BINDIR)/$(SRC_BIN) : $(SRC_OBJ)
—$(CC) -o $@ $^ $(LDFLAGS)
endif
ifneq ($(SRC_LIB),)
$(LIBDIR)/$(SRC_LIB) : $(SRC_OBJ)
—$(AR) rcs $@ $^
—cp $@ $(SRC_BASE)/libs
endif
$(SRC_OBJ) : $(OBJDIR)/%.o : %.c
—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
msg:
—@echo “You cannot directily execute this Makefile! This Makefile should called by toplevel Makefile.”
clean:
ifneq ($(SRC_LIB),)
—>—$(RM) $(SRC_OBJ) $(LIBDIR)/$(SRC_LIB)
endif
ifneq ($(SRC_BIN),)
—>—$(RM) $(SRC_OBJ) $(BINDIR)/$(SRC_BIN)
endif
.PHONY : all clean
1 |
|
SRC_FILES = $(wildcard src/*.c)
1 | 返回src目录下所有.c文件列表。 |
SRC_OBJ = $(patsubst %.c, %.o, $(SRC_FILES))
1 | 将SRC_FILES中所有.c文件替换为.o返回给变量SRC_OBJ。 |
$(objects:.c=.o)
$(patsubst %.c,%.o,$( src_files))
1 | 4. 过滤函数—filter。 |
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/ipc'
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/ipc’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/tools'
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/tools’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/main'
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/main’
1 | 发现只有进入目录和退出目录的显示,这样很难知道目前编译过程。其实我们可以在规则命令处加入一行类似打印: |
$(OBJDIR) :
—@echo “ MKDIR $(notdir $@)…”
—@mkdir -p $@
ifneq ($(SRC_BIN),)
$(BINDIR)/$(SRC_BIN) : $(SRC_OBJ)
—@echo “ LINK $(notdir $@)…”
—@$(CC) -o $@ $^ $(LDFLAGS)
endif
ifneq ($(SRC_LIB),)
$(LIBDIR)/$(SRC_LIB) : $(SRC_OBJ)
—@echo “ ARCHIVE $(notdir $@)…”
—@$(AR) rcs $@ $^
—@echo “ COPY $@ to $(SRC_BASE)/libs”
—@cp $@ $(SRC_BASE)/libs
endif
$(SRC_OBJ) : $(OBJDIR)/%.o : %.c
—@echo “ COMPILE $(notdir $<)…”
—@$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
1 | 编译输出如下: |
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/ipc'
COMPILE ipc.c...
ARCHIVE libipc.a...
COPY ../../build/unix_dbg/lib/libipc.a to ../../libs
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/ipc’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/tools'
COMPILE base64.c...
COMPILE md5.c...
COMPILE tools.c...
ARCHIVE libtools.a...
COPY ../../build/unix_dbg/lib/libtools.a to ../../libs
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/tools’
make[1]: Entering directory /home/Myprojects/example_make/version-3.1/src/main'
COMPILE main.c...
LINK target_bin...
make[1]: Leaving directory
/home/Myprojects/example_make/version-3.1/src/main’
1 | 其中目录切换的输出仍然很多,我们可以将其关闭,这需要使用到make的参数,在make -C是指定--no-print- |
$(BUILDDIR):
—@echo “ Create directory $@ …”
—mkdir -p $(BUILDDIR)/bin $(BUILDDIR)/lib
$(MODULES):
—@$(MAKE) -C $(DIR)/$@ MODULE=$@ –no-print-directory
main:tools ipc
clean :
—@for subdir in $(MODULES); \ —do $(MAKE) -C $(DIR)/$$subdir MODULE=$$subdir $@ –no-print-directory; \ —done
编译输出:
COMPILE ipc.c...
ARCHIVE libipc.a...
COPY ../../build/unix_dbg/lib/libipc.a to ../../libs
COMPILE base64.c...
COMPILE md5.c...
COMPILE tools.c...
ARCHIVE libtools.a...
COPY ../../build/unix_dbg/lib/libtools.a to ../../libs
COMPILE main.c...
LINK target_bin…
rm -f ../../build/unix_dbg/obj/ipc/ipc.o ../../build/unix_dbg/lib/libipc.a
rm -f ../../build/unix_dbg/obj/main/main.o ../../build/unix_dbg/bin/target_bin
rm -f ../../build/unix_dbg/obj/tools/base64.o ../../build/unix_dbg/obj/tools/md5.o
../../build/unix_dbg/obj/tools/tools.o ../../build/unix_dbg/lib/libtools.a
#
这样看上去输出清爽多了。其实我们也可以使用make -s 来全面禁止命令的显示。
>【版权声明:转载请保留出处:http://blog.csdn.net/shallnet/article/details/37358655】