老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

android-phone-compatibility

发表于 2017-11-02 | 分类于 Android

系统摄像视频文件格式

一般手机使用摄像头录制视频格式为yuv420p,而小米5录制出的为yuvj420p.格式转换是yuvj420p当成yuv420p处理即可.

ndk编译常见问题

发表于 2017-11-01 | 分类于 Android

depends on undefined modules

问题:

1
2
Users/shenjunwei/program/android-ndk-r14b/build/core/build-binary.mk:687: Android NDK: Module magicsdk_fmod depends on undefined modules: cutils
/Users/shenjunwei/program/android-ndk-r14b/build/core/build-binary.mk:700: *** Android NDK: Aborting (set APP_ALLOW_MISSING_DEPS=true to allow missing dependencies) . Stop.

解决方案: Android.mk中增加APP_ALLOW_MISSING_DEPS=true

shared library text segment is not shareable

问题:

1
2
3
4
/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
/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: error: treating warnings as errors
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [/Users/shenjunwei/Documents/repository/wonxing/normandy_android_app/modules-int/magicsdk_core/src/main/obj/local/armeabi-v7a/libmagicsdk_ex.so] Error 1

解决:

1
2
3
4
5
6
from Android NDK r11 you can use

LOCAL_LDLIBS += -Wl,--no-warn-shared-textrel
You can also use

LOCAL_DISABLE_FATAL_LINKER_WARNINGS := true

shared library text segment is not shareable

has text relocations

问题:

1
2
3
4
5
6
7
8
9
10
/data/app/com.wonxing.touchfa-2/lib/arm/libmagicsdk_ex.so: has text relocations
E/FileUtil: access inferno failed! /data/app/com.wonxing.touchfa-2/lib/arm/libmagicsdk_ex.so
java.lang.UnsatisfiedLinkError: dlopen failed: /data/app/com.wonxing.touchfa-2/lib/arm/libmagicsdk_ex.so: has text relocations
at java.lang.Runtime.load0(Runtime.java:897)
at java.lang.System.load(System.java:1505)
at com.wonxing.magicsdk.core.util.FileUtil$EXLibUtil.load(FileUtil.java:465)
at com.wonxing.magicsdk.core.MagicRecorder.loadEXLibrary(MagicRecorder.java:280)
at com.wonxing.magicsdk.core.MagicRecorder.prepare(MagicRecorder.java:471)
at com.wonxing.magicsdk.core.MagicRecorder.prepare(MagicRecorder.java:352)
at com.wonxing.touchfa.ui.activity.VideoImportActivity.preparePlaySDK(VideoImportActivity.java:144)

解决:

  1. 方案一 This issue could be solved by checking the targetSDKVersion in the manifest file.

Using “22” and not “23” as targetSDKVersion solved it. (See below)

1
2
3
<uses-sdk
android:minSdkVersion="15"
android:targetSdkVersion="22" />

I also checked the build.gradle files for compile version and targetSDKversion:

1
2
3
4
5
6
7
compileSdkVersion 22
buildToolsVersion '22.0.1'

defaultConfig {
minSdkVersion 15
targetSdkVersion 22
}
  1. 方案二 It was caused by the ffmpeg, and it could also be solved by patching the latest ffmpeg code
    1
    2
    3
    4
    5
    libavcodec\arm\fft_fixed_neon.S
    libavcodec\arm\fft_neon.S
    libavcodec\arm\fft_vfp.S
    libavcodec\arm\mlpdsp_armv5te.S
    libutil\arm\asm.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.

  1. 方案三(Further informations:) Previous versions of Android would warn if asked to load a shared library with text relocations:

“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.

Android Developers Blog Hardened/Textrels Guide

最近应用杀掉进程application不销毁问题探讨

发表于 2017-10-31 | 分类于 Android

建雨在芝士圈应用的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
2
3
4
5
6
7
8
9
10
11
12
13
/**
* This is called if the service is currently running and the user has
* removed a task that comes from the service's application. If you have
* set {@linkandroid.content.pm.ServiceInfo#FLAG_STOP_WITH_TASK ServiceInfo.FLAG_STOP_WITH_TASK}
* then you will not receive this callback; instead, the service will simply
* be stopped.
*
*@paramrootIntentThe original root Intent that was used to launch
* the task that is being removed.
*/

public void onTaskRemoved(Intent rootIntent) {
}

注释表明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
2
3
04-26 16:20:00.349 32674-32674/? D/qintong: Service onTaskRemoved.
04-26 16:21:01.621 2936-2936/? D/qintong: Service is being created.
04-26 16:21:01.628 2936-2936/? D/qintong: Service onStartCommand.

(b).

1
2
04-21 17:59:58.397 8264-8264/com.qintong.test D/qintong: Service onCreate.
04-21 17:59:58.404 8264-8264/com.qintong.test D/qintong: Service onTaskRemoved.

浅杀+深杀 (service 的 onStartCommand 返回 STICKY):

1
2
3
04-21 18:05:12.717 8264-8264/com.qintong.test D/qintong: Service onTaskRemoved.
04-21 18:05:29.214 9207-9207/com.qintong.test D/qintong: Service onCreate.
04-21 18:05:29.223 9207-9207/com.qintong.test D/qintong: Service onStartCommand.

我们来分析这几种情况: (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void onCreate() {
super.onCreate();
mBinder=newMyBinder();
if(DEBUG) Log.d(LOG_TAG,"vCardService is being created.");
mNotificationManager= ((NotificationManager)getSystemService(NOTIFICATION_SERVICE));
initExporterParams();
}

@Override
public int onStartCommand(Intent intent, intflags, intid) {
if(DEBUG) Log.d(LOG_TAG,"vCardService onStartCommand.");
mNotificationManager.cancelAll();
return START_STICKY;
}

@Override
public void onTaskRemoved(Intent rootIntent) {
if(DEBUG) Log.d(LOG_TAG,"vCardService onTaskRemoved.");
mNotificationManager.cancelAll();
super.onTaskRemoved(rootIntent);
}

如上代码,在浅杀时候:只执行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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void cancelNotification() {
if (foregroundId != 0) {
// Do asynchronous communication with notification manager to
// avoid deadlocks.
final String localPackageName = packageName;
final int localForegroundId = foregroundId;
ams.mHandler.post(new Runnable() {
public void run() {
INotificationManager inm = NotificationManager.getService();
if (inm == null) {
return;
}
try {
inm.cancelNotificationWithTag(localPackageName, null,
localForegroundId, userId);
} catch (RuntimeException e) {
Slog.w(TAG, "Error canceling notification for service", e);
} catch (RemoteException e) {
}
}
});
}
}

Kotlin语法

发表于 2017-10-31 | 分类于 language

字符串比较

1
2
3
var str1 = "chaychan"
var str2 = "chaychan"
println(str1 == str2)

比较两个字符串,如果两个字符串的内容一致,在Java中使用 str1 == str2 时,是比较两个字符串的地址值,很清楚两个字符串的地址不一样,返回false,但是在kotlin中,则不是如此,比较的只是字符串的内容,而===相当于Java中的==,用来比较引用对象, 上述代码返回的是true。

equal函数

  1. equals(str:String)

方法中的参数是与之对比的字符串,默认不忽略大小写,即大小写敏感,比如:

1
2
3
var str1 = "chaychan"
var str2 = "ChayChan"
println(str1.equals(str2))

打印结果为false,因为不忽略大小写的话,两个字符串内容对比是不一致的,所以返回false。

  1. equals(str:String,ignoreCase:Boolean) 方法中有两个参数,第一个参数是与之对比的字符串,第二个参数是布尔类型的值,是否忽略大小写,如:
    1
    2
    3
    var str1 = "chaychan"
    var str2 = "ChayChan"
    println(str1.equals(str2,true))

返回结果为true。

源码优化分析

源码

1.Lateinit

在View声明阶段,都会需要使用lateinit来延迟声明变量。

1
2
3
4
5
class TaskActivity : AppCompatActivity(){
private val CURRENT_FILTERING_KEY = "CURRENT_FILTERING_KEY";
private lateinit var drawerLayout : DrawerLayout
private lateinit var tasksPresenter:TasksPresenter
}

kotlin中延迟声明还包括lazy的方式

1
2
val name:String by lazy {"cangwang"}
lateinit var drawLayout:drawLayout

区别在于:

  1. .lazy{}只能用再val类型,lateinit只能用在var类型
  2. .lateinit不能用在可空的属性上和java的基本类型上lateinit var name:String会报错
  3. .lateinit可以在任何位置初始化并且可以初始化多次,因为其衔接var变量.而lazy在第一次被调用时就被初始化,其衔接的是val常量,想要被改变只能重新定义

2.findViewById

Api26前:

1
2
3
4
@Override
public View findViewById(@IdRes int id){
return getDelegate().findViewById(id);
}

Api26之后

1
2
3
4
5
@SuppressWarnings("TypeParameterUnusedInFormals")
@Override
public <T extends View> T indViewById(@IdRes int id){
return getDelegate().findViewById(id);
}

五个kotlin Standard.kt里面的函数:apply,with,let,run,also

  1. apply作用
    1
    2
    3
    4
    5
    setSupportActionBar(findViewById<Toolbar>(R.id.toolbar))
    supportActionBar?.apply{
    setDisplayHomeAsUpEnabled(true)
    setDisplayShowHomeEnabled(true)
    }

在函数内可以通过this指代该对象,返回值为该对象自己

  1. with函数 将某对象作为函数的参数,在函数内可以通过this指代该对象.返回值为函数块的最后一行或指定return表达式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    override fun getView(i:Int,view:View?,viewGroup:ViewGroup):View{
    val rowView=Vview?:LayoutInflater.from(viewGroup.context).inflate(R.layout.task_item,viewGroup,false)
    val task = getItem(i)
    with(rowView.findViewById<TextView>(R.id.title)){
    text = task.titleForList
    }
    with(rowView.findViewById<CheckBox>(R.id.complete)){
    isChecked=task.isCompleted
    rowView.setBackgroundDrawable(...)
    setOnClickListener{

    }
    }
    }

3.lat函数

1
2
3
4
5
private fun showMessage(message:String){
view?.let{
Snackbar.make(it,message,Snackbar.LENGTH_LONG).show()
}
}

将对象作为函数参数,在函数块内可以通过it指代该对象.返回值为函数块的最后一行或指定return表达式

4. run函数

其有两种表达式:

  • 第一种无参数输入
  • 第二种会将对象本身this给函数调用 返回值为函数块最后一行,或者指定return表达式

Object

单例对象是使用Object申明 Kotlin没有静态属性和方法,需要使用单例对象来实现类似的功能.

data

相当于java中定义的数据bean类,其可以直接在属性之后编写get和set方法

@JvmOverloads

(转)转使用C语言实现"泛型"链表

发表于 2017-10-31 | 分类于 language

看到这个标题,你可能非常惊讶,C语言也能实现泛型链表?我们知道链表是我们非常常用的数据结构,但是在C中却没有像C++中的STL那样有一个list的模板类,那么我们是否可以用C语言实现一个像STL中的list那样的泛型链表呢?答案是肯定的。下面就以本人的一个用C语言设计的链表为例子,来分析说明一下本人的设计和实现要点,希望能给你一点有用的帮助。

一、所用的链表类型的选择

我们知道,链表也有非常多的类型,包括单链表、单循环链表、双链表、双向循环链表等。在我的设计中,我的链表使用的类型是双向循环链表,并带一个不保存真实数据的头结点。其原因如下: 1)单链表由于不能从后继定位到前驱,在操作时较为不方便 2)双链表虽然能方便找到前驱,但是如果总是在其尾部插入或删除结点,为了定位的方便和操作的统一(所有的删除和插入操作,都跟在中间插入删除结点的操作一样),还要为其增加一个尾结点,并且程序还要保存一个指向这个尾结点的指针,并管理这个指针,从而增加程序的复杂性。而使用带头结点的循环双向链表,就能方便的定位(其上一个元素为链表的最后一个元素,其下一个元素为链表的第0个元素),并使所有的插入和删除的操作统一,因为头结点也是尾结点。注:结点的下标从0开始,头结点不算入下标值。 3)接口的使用与C++中stl中list和泛型算法的使用大致相同。

二、list类型的定义

为了让大家一睹为快,下面就给出这个用C语言实现的“泛型”的定义,再来说明,我这样设计的原因及要点,其定义如下: 其定义在文件list_v2.c中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct node  
{
//循环双链表的结点结构
void* data;//数据域指针
struct node *next;//指向当前结点的下一结点
struct node *last;//指向当前结点的上一结点
}Node;

struct list
{
struct node *head;//头指针,指向头结点
int data_size;//链表对应的数据所占内存的大小
int length;//链表list的长度
};

其声明在文件list_v2.h中

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//泛型循环双链表,带头结点,结点下标从0开始,头结点不计入下标值  


//定义结点指针Node*为List类型的迭代器
typedef struct node* Iterator;


//List类型的定义
typedef struct list* List;


//初始化链表,数据域所占内存的大小由data_size给出
int InitList(List *list, int data_size);


//把data的内容插入到链表list的末尾
//assign指定数据data间的赋值方法
Iterator Append(List list, void *data,
void (*assign)(void*, const void*));


//把data的内容插入到链表的迭代器it_before的前面
//assign指定数据data间的赋值方法
Iterator Insert(List list, void *data, Iterator it_before,
void (*assign)(void*, const void*));


//把链表A中迭代器it_a指向的结点移动到链表B中迭代器it_b_befroe的前面
Iterator MoveFromAtoB(List A, Iterator it_a,
List B, Iterator it_b_before);


//删除链表list中迭代器it指向的结点
int Remove(List list, Iterator it);


//删除链表list的第0个结点,下标从0开始
int RemoveFirst(List list);


//删除链表list的最后一个结点
int RemoveLast(List list);


//返回list中第index个数据的指针
void* At(List list, int index);


//在begin和end之间查找符合condition的第一个元素,
//比较函数由condition指向,比较的值由data指向
//当第一个参数的值小于第二个参数的值时,返回1,否则返回0
//根据condition函数的不同,可以查找第一个相等、大于或小于data的值
Iterator FindFirst(Iterator begin, Iterator end, void *data,
int (*condition)(const void*, const void*));


//查找list中第一个与data相等的元素的下标,
//equal函数,当第一个参数与第二个参数的值相等时,返回1,否则返回0
int IndexOf(List list, void *data,
int (*equal)(const void*,const void*));


//查找在begin和end之间的最小值,比较函数由less指向
//当第一个参数的值小于第二个参数的值时,返回1,否则返回0
Iterator GetMin(Iterator begin, Iterator end,
int (*less)(const void*, const void*));


//查找在begin和end之间的最大值,比较函数由large指向
//当第一个参数的值大于第二个参数的值时,返回1,否则返回0
Iterator GetMax(Iterator begin, Iterator end,
int (*large)(const void*, const void*));


//获取list的长度
int GetLength(List list);
//若list为空链表,则返回1,否则返回0
int IsEmpty(List list);
//销毁list
void DestroyList(List *list);


//获得list的首迭代器
Iterator Begin(List list);


//获得list的尾迭代器,指向最后一个元素的下一个位置
Iterator End(List list);


//使it指向下一个位置,并返回指向下一个位置后的迭代器
Iterator Next(Iterator *it);


//使it指向上一个位置,并返回指向上一个位置后的迭代器
Iterator Last(Iterator *it);


//通过迭代器it获得数据,相当于*p
void* GetData(Iterator it);


//获取当前迭代器的下一个迭代器,注意,并不改变当前迭代器
Iterator GetNext(Iterator it);


//获取当前迭代器的上一个迭代器,注意,并不改变当前迭代器
Iterator GetLast(Iterator it);

三、如何实现隐藏链表的成员变量(即封装)

首先,我们为什么需要封装呢?我觉得封装主要有三大好处。

  1. 隔离变化,在程序中需要封装的通常是程序中最容易发生变化的地方,例如成员变量等,我们可以把它们封装起来,从而让它们的变化不会影响到系统的其他部分,也就是说,封装的是变化。
  2. 降低复杂度,因为我们把一个对象是如何实现的等细节封装起来,只留给用户一个最小依赖的接口,从而让系统变量简单明了,在一定程度降低了系统的复杂性,方便了用户的使用。
  3. 让用户只能按照我们设计好的接口来操作一个对象或类型,而不能自己直接对一个对象进行操作,从而减少了用户的误操作,提高了系统的稳定性。

在面向对象的设计中,如果我们想要隐藏一个类的成员变量,我们可以把这些成员变量声明为私有的,而在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所占的内存大小。

五、为什么需要赋值函数指针assign

这里来说明一下,该链表的数据的插入方式,我们的插入方式是,新建一个结点,把data指向的数据复制到结点中,并把该结点插入到链表中。插入的函数定义如下:

1
2
Iterator Insert(List list, void *data, Iterator it_before,
void (*assign)(void*, const void*));

从上面的解说中,我们可以看到链表中的成员data_size指示了链表中的数据所占的内存大小,那我们们就可以使用函数memcpy把data指向的数据复制到新建的结点的data所指向的内存即可。为什么还需要一个函数指针assign,来指向一个定义数据之间如何赋值的函数呢?其实这和面向对象语言中常说到的深复制和浅复制有关。

注:memcpy函数的原型为:void * memcpy ( void * destination, const void * source, size_t num );

试想一下,假如你的链表的数据类型不是int型等基本类型,也不是不含有指针的结构体,而是一个这样的结构体,例如:

1
2
3
4
5
6
struct student  
{
char *name;
char *no;
int age;
};

学生的姓名和学号都是能过动态分配内存而来的,并由student结构体中的name和no指针指向,那么当我们使用memcpy时,只能复制其指针,而不能复制其指向的数据,这样在很多情况下都会带来一定的问题。这个跟在C++中什么时候需要自己定义复制构造函数的情况类似。因为这种情况下,默认的复制构造函数并不能满足我们的需要,只能自己定义复制构造函数。

所以在插入一个结点时,需要assign函数指针的原理与C++中自己定义复制构造函数的原理一样。它用于定义如何根据一个已有的对象生成一个该对象的拷贝对象。当然,可能在大多数的情况下,我们需要用到的数据类型都没有包含指针,所以在Insert函数的实现中,其实我也是有用到memcpy函数的,就是当assign为NULL时,就使用memcpy函数进行数据对象间的赋值,它其实就相当于C++中的默认复制构造函数或默认赋值操作函数。assign为NULL表示使用默认的逐位复制方式,即浅复制。

六、为什么不用typedef

对于这个问题,其实很好回答。很多人实现一个通用链表是这样实现的,它们把node结构的实现如下:

1
2
3
4
5
6
7
typedef struct node  
{
//循环双链表的结点结构
DataType data;//数据域指针
struct node *next;//指向当前结点的下一结点
struct node *last;//指向当前结点的上一结点
}Node;

然后,当需要使用整型的链表时,就把DataType用typedef为int。其实这样做的一个最大的缺陷就是一个程序中只能存在着一个数据类型的链表,例如,如果我需要一个int型的链表和一个float型的链表,那么该把DataType定义为int呢还是float呢?所以这种看似可行的方式,其实只是虚有其表,在现象中是行不能的,虽然不少的数据结构的书都是这样实现的,但是它却没有什么实用价值。

而其本质的原因是把结点的数据域的数据类型与某一种特定的数据类型DataType绑定在一起,从而让链表不能独立地变化。

七、为什么只把结点的指针定义为Iterator

在C++中iterator是一个类,为什么在这里,我只把结点的指针声明为一个Iterator呢?其实受STL的影响,我在一开始时,也是把Iterator实现为一个结构体,它只有一个数据成员,就是一个指向Node的指针。但在后来的实践中,发现其实并没有必要。在C++中为什么把iterator定义为一个类,是为了重载*,->等运行符,让iterator使用起来跟普通的指针一样。但是在C语言中,并没有重载运行符的做法,所以直接把Ierator声明为一个Node的指针最为方便、直接和好用,所有的比较运算都可以直接进行,而无需要借助函数。而把它声明为一个结构体反而麻烦、累赘。

八、为什么查找需要两个Iterator

其实这是参考了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的位置。

转自使用C语言实现“泛型”链表

AndroidStudio IDE

发表于 2017-10-31 | 分类于 工具

AndroidStudio3.0新特性

支持Java8语言

由于AS3.0默认支持Java8语言,所以我们就可以移除build.gradle里面的jackOptions了 jackOptions { true }

然后可以在build.gradle配置为Java8

1
2
3
4
5
6
7
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

如果对Java8的一些特性存在问题,我们也可以在gradle.properties里面禁用Java8

1
android.enableDesugar=false

配置产品渠道

AS3.0以前我们常用productFlavors配置不同的渠道包,比如

1
2
3
4
5
6
7
8
9
productFlavors {
dev{
applicationIdSuffix ".dev"
...
}
prod {
...
}
}

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
2
3
4
defaultConfig {
...
flavorDimensions "debug","release"
}

然后productFlavors的配置就可以如下:

1
2
3
4
5
6
7
8
9
10
11
productFlavors {
demo {
dimension "debug"
applicationIdSuffix ".demo"
...
}
prod {
dimension "release"
...
}
}

改进的Android插件

  • 优化了多 module 的项目并行编译运行更详细Task的展示 构建变体的从属管理,比如上文的Flavors Dimensions配置新 api ,implementation依赖(替代compile ),compileOnly(替代provided)和runtimeOnly(替代 apk)
  • 通过增量编译 优化多dex的app构建速度
  • 优化了AAPT2增量资源化处理。如果要启用AAPT2,在gradle.properties文件添加代码:android.enableAapt2=true
  • 支持java8语言
  • 增加测试工具,可通过dependencies依赖使用
    1
    2
    3
    4
    dependencies{
    androidTestUtil“com.linkedin.testbutler:测试管家应用:1.3.0@apk”
    ...
    }

常见出错总结

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
2
3
4
5
6
apk adb install '/home/silver/桌面/share/apk/app-android-debug.apk' 
adb server is out of date. killing...
* daemon started successfully *
5347 KB/s (8754206 bytes in 1.598s)
pkg: /data/local/tmp/app-android-debug.apk
Failure [INSTALL_FAILED_TEST_ONLY]

但是使用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

Unexpected scopes found in folder

1
2
3
4
Error:Execution failed for task ':app:transformClassesWithExtractJarsForDebug'.
> Unexpected scopes found in folder 'C:\my_demo_test\TestDemo\app\build\intermediates\transforms\AspectTransform\debug'.
Required: SUB_PROJECTS. Found:
EXTERNAL_LIBRARIES, PROJECT, SUB_PROJECTS

解决方案: 造成上述问题是由于as版本,需将Android studio的instant run关闭。具体如下: Settings → Build, Execution, Deployment → Instant Run and uncheck Enable Instant Run.

C/CPP中的编程技巧及其概念

发表于 2017-10-17 | 分类于 language

C Language

size_t

size_t的全称应该是size type,就是说“一种用来记录大小的数据类型”。属于C99标准,它所定义的变量可以进行加减乘除运算。因此函数中表示数据大小的变量,推荐使用这个类型!例如:

1
int xxx(voidvoid *p, size_t len);

指针的指针(双重指针)的作用:

  1. 用来传递需要修改的指针参数到函数中;
  2. 用来动态生成多维数组;
  3. 多用于指针交换,可以避免数据复制,提升系统的性能,同时还可以让函数修改指针,例如扩充其大小,指向等一般指针的指针用作参数,大多用在需要函数改变指针(重新引用变量)而又不能通过返回值传递(例如返回值用于传递其他结果)时。

内联函数

以空间换时间。

backtrace函数追踪函数调用堆栈以及定位段错误

一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的

CPP

显示限定数组实参的原始个数

数组在作为函数参数传递时会退化为指针:

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
2
3
void funcA(int x[10]){}
// Equivalent to
void funcB(int *x){}

其对应的中间代码为:

1
2
3
4
5
6
7
8
9
10
11
12
; Function Attrs: nounwind uwtable
define void @_Z5funcAPi(i32*) #4 {
%2 = alloca i32*, align 8
store i32* %0, i32** %2, align 8
ret void
}
; Function Attrs: nounwind uwtable
define void @_Z5funcBPi(i32*) #4 {
%2 = alloca i32*, align 8
store i32* %0, i32** %2, align 8
ret void
}

如果数组边界的精确数值非常重要,并且希望函数只接收含有特定数量的元素的数组,可以使用引用形参:

1
void funcC(int (&x)[10]){}

其中间代码为:

1
2
3
4
5
6
; Function Attrs: nounwind uwtable
define void @_Z5funcCRA10_i([10 x i32]* dereferenceable(40)) #4 {
%2 = alloca [10 x i32]*, align 8
store [10 x i32]* %0, [10 x i32]** %2, align 8
ret void
}

如果我们使用数组元素个数不等于10的数组传递给funcC,会导致编译错误:

1
2
3
4
5
6
7
8
9
// note: candidate function not viable: no known conversion from 'int [11]' to 'int (&)[10]' for 1st argument.
void funcC(int (&x)[10]){}
int main(int argc,char* argv[])
{
int x[11]={0,1,2,3,4,5,6,7,8,9,10};
// error: no matching function for call to 'funcC'.
funcC(x);
return 0;
}

也可以使用函数模板参数来指定函数接收参数的数组大小:

1
2
template<int arrSize>
void funcA(int x[arrSize]){}

使用时:

1
2
3
int x[12]
funcA<12>(x); // OK
funcA<13>(x); //ERROR

启用编译器的改变符号的隐式类型转换警告

1
2
3
4
5
if((unsigned int)4<(unsigned int)(int)-1){
cout<<"yes"<<endl;
}else{
cout<<"no"<<endl;
}

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)

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
2
3
4
5
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif

assert只在Debug模式中有效,使用release模assert什么都不做了。 因为在VC++里面,release会在全局定义NDEBUG 下面的代码在VS中使用debug和release模式分别编译并输入>100的数,会有不一样的结果(release不会)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
bool func(int x) {
if (x > 100) {
return true;
}
else {
return false;
}
}
int main(void) {
int i;
cin >> i;
assert(func(i));
}

无效的引用

通常情况下我们创建的引用就是有效的,但是也可以人为因素使坏…

1
2
3
4
5
6
char* ident(char *p) { return p; }
int main(int argc,char* argv[])
{
char& r {*ident(nullptr)};
return 0;
}

这是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
2
3
4
5
6
7
8
9
void f(int(&r)[4]){
cout<<sizeof(r)<<endl;
}
void g(void){
int a[]={1,2,3,4};
f(a); // OK
int b[]={1,2,3};
f(b); // 错误,元素个数有误
}

对于数组引用类型的从参数来说,元素个数也是其类型的一部分。通常只有在模板中才会使用数组引用,此时数组的引用可以通过推断得到。

1
2
3
4
5
6
7
8
9
10
template<class T,int N>
void f(T(&r)[N]){
// ...
}
int a1[10];
double a2[100];
void g(){
f(a1); // T是int,N是10
f(a2); // T是double,N是100
}

这么做的后果是调用f()所用的不同类型的数组有多少个,对应定义的函数有多少个。

忽略函数参数的顶层const

为了与C语言兼容,在C++中会自动忽略参数类型的顶层const。

例如下面的函数在C++会报重定义错误,而不是重载:

1
2
3
4
5
// 类型是int(int)
int f(int x){}
// error: redefinition of 'f'
// 类型是int(int)
int f(const int x){}

不论对于哪种情况,允许修改实参也好,不允许修改实参也好,它都只是函数调用者提供的实参的一个副本。因此调用过程不会破坏调用上下文的数据安全性。

char作为数组下标时当心unsigned/signed

当char类型用作数组下标时,一定要先转unsigned char(因为char通常是有符号的(依赖实现定义))。不能直接转int或unsigned int,会数组下标越界。

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void) {
char ch=-1;
printf("%d %u %d", (int)ch, (unsigned)ch, (unsigned char)ch);
return 0;
}
// output
// -1 4294967295 255

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.

new一个指针数组

1
2
3
int TEN=10;
auto A=new (void(*[TEN])(void));
delete[] A;

底层(Low-Level)const和顶层(Top-Level)const

  • 底层const(Low-Level const):表示指针所指的对象是一个常量。
  • 顶层const(Top-Level const):表示指针本身是个常量。顶层const可以表示任意的对象是常量,这对于任何数据类型都适用。
    1
    2
    3
    4
    5
    6
    int ival=0;
    int *const ivalp_1=&ival; // 不能改变ivalp_1的值,这是一个顶层const
    const int ci=42; // 不能改变ci的值,这是一个顶层const
    const int *ivalp_2=&ci;; // 允许改变ivalp_2的值,这是一个底层const
    const int *const ivalp_3=ivalp_2; //靠右的是顶层const,靠左的是底层const
    const int &ref=ci; // 用于声明引用的const都是底层const

其实我有一个简单的区分的方法:看const修饰的右边是什么。

  • 对于int const *x=std::nullput;,const修饰的是x,因为x是指针,我们就暂且把此处的x当做解引用来看,他就代表x所指向的对象,则它就是底层const。
  • 反之亦然,int * const x=std::nullptr;,因为const修饰的是指针x,所以它就是顶层const。

在构造函数中传递this指针的危害

如果我们在构造函数中将this指针传递给其它的函数,有可能会引发这样的问题:

1
2
3
4
5
6
struct C;
void no_opt(C*);
struct C {
int c;
C() : c(0) { no_opt(this); }
};

看起来上面的代码似乎没什么问题,但是我们构造一个const C的时候,有可能会出现这样的问题:

1
2
3
4
5
6
7
const C cobj;
void no_opt(C* cptr) {
int i = cobj.c * 100; // value of cobj.c is unspecified
cout<<i<<endl;
cout << cobj.c * 100 // value of cobj.c is unspecified
<< '\n';
}

上面的代码会编译通过并可以在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
2
3
4
#include <direct.h>
char buffer[MAXPATH];
getcwd(buffer, MAXPATH);
cout<<buffer<<endl;

这种方法有一个弊端:如果将可执行程序添加至系统的PATH路径,则获取到的是在某个目录执行时该目录的路径。

另一种方法是通过Windows API来获取:

1
2
3
4
5
6
const string getTheProgramAbsPath(void){
TCHAR exeFullPath[MAX_PATH]; // MAX_PATH在WINDEF.h中定义了,等于260
memset(exeFullPath,0,MAX_PATH);
GetModuleFileName(NULL,exeFullPath,MAX_PATH);
return {exeFullPath};
}

在此种方式下不论是否将该程序添加至系统的PATH路径以及在何处执行,都会获取该可执行程序在系统中存放的绝对路径。

一个奇葩的using用法

1
2
3
4
5
using foofunc=void(int);
foofunc foo;
int main(){
foo(1);
}

上面的代码里:

1
foofunc foo;

是声明一个函数foo,可以看一下目标文件中的符号信息(省去无关细节):

1
2
3
4
5
6
$ clang++ -c testusing.cc -o testusing.o -std=c++11
$ llvm-nm testusing.o
-------- U _Z3fooi
-------- U __main
-------- U atexit
00000050 T main

通过gcc工具链中的c++filt可以还原目标文件中的符号:

1
2
$ c++filt _Z3fooi
foo(int)

但是并没有定义,直接链接会产生未定义错误。

右值引用

1
2
int x=123;
int &&y=x+1;

其IR代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用值123初始化x
%2 = alloca i32, align 4
store i32 123, i32* %2, align 4
# y
%3 = alloca i32*, align 8
# 存放x+1产生的临时对象
%4 = alloca i32, align 4
# 计算x+1
%5 = load i32, i32* %2, align 4
%6 = add nsw i32 %5, 1
# x+1 产生一个临时值,该临时值为%4
store i32 %6, i32* %4, align 4
# 将该临时值的地址绑定到%3(y)
store i32* %4, i32** %3, align 8

从而实现非拷贝行为,其行为类似于将一个对象的地址赋值给一个指针。 其实右值引用的作用就是给临时对象续命——将引用绑定到一个临时对象,不会带来额外的拷贝操作。 实现同样续命行为的还有const T&:

1
2
int x=123;
const int &y=x+1;

和上面的示例在LLVM下会产生一模一样的IR代码。

一个数组名字例子

1
2
3
4
int a[]={1,2,3,4,5};
int *p=(int*)(&a+1);
printf("%d,%d\n",*(a+1),*(p-1));
// output: 2,5

到底有几种传参方式

大多数人都觉得在C++函数中有以下三种传参方式:

  • 传值(by value):形参的值是实参的拷;
  • 传引用(by reference):形参是实参的别名;
  • 传指针(by pointer):传递指向对象的指针给形参; 实际上,C++中只有两种传参方式:传值、传引用。 因为传指针(by pointer)也是传值的一种,形参的值也只是实参的一份拷贝,只是形参和实参都是指针而已。 在C++之父的著作:《The C++ Programming Language 4th》中写道:

    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A{
A():memory(nullptr){}
void getMemory(std::size_t memSize){
memory=(char*)malloc(memSize);
}
~A(){ free(memory); }
private:
char* memory;
};
int main()
{
A x;
x.getMemory(12);
A y;
y=x;
}

如果使用编译器生成的语义会使对象x和y内部共享一块内存,所以需要用户自己定义拷贝构造和拷贝赋值操作符,同样的原因,因为类成员持有某种资源,也需要用户自定义一个析构函数。

引用的实现

C++标准中是这么解释引用的:

[ISO/IEC 14882:2014 §8.3.2]A reference can be thought of as a name of an object.

但是标准中并没有要求应该如何实现引用这一行为(这一点标准中比比皆是),不过多数编译器底层都是使用指针来实现的。 看下列代码:

1
2
3
int a=123;
int &ra=a;
int *pc=&a;

然后将其编译为LLVM-IR来看编译器的实际行为:

1
2
3
4
5
6
%2 = alloca i32, align 4
%3 = alloca i32*, align 8
%4 = alloca i32*, align 8
store i32 123, i32* %2, align 4
store i32* %2, i32** %3, align 8
store i32* %2, i32** %4, align 8

可以看到,指针和引用在经过编译器之后具有了完全相同的行为。

适当使用编译器生成操作

在特殊成员函数的隐式声明及其标准行为中提到了编译器会隐式生成和定义六种特殊的成员函数的行为。 因为编译器生成的copy constructor和copy assigment operator均是具有memberwise行为的。所以当我们撰写的类使用浅拷贝可以满足的时候(值语义),没必要自己费劲再写相关的操作了,因为编译器生成的和你手写的一样好,而且不容易出错。

1
2
3
4
5
6
7
struct A{
A(int a=0,double b=0.0):x(a),y(b){}
A(const A&)=default;
A& operator=(const A&)=default;
int x;
double y;
};

虽然当你没有显式定义一个copy constructor和copy assignment operator的时候编译器就会隐式定义,但是最好还是自己手动使用=delete指定。 编译器生成的和下面这样手写的一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct A{
A(int a=0,double b=0.0):x(a),y(b){}
A(const A& r){
x=r.x;
y=r.y;
}
A& operator=(const A& r){
x=r.x;
y=r.y;
return *this;
}
int x;
double y;
};

显然自己手写容易出错,这样的行为可以放心地交给编译器来做。

STL容器中压缩容量和真正地删除元素

摘取自《C++编程规范:101条规则/准则与最佳实践》第82条。

压缩容器容量:swap魔术
1
2
3
4
vector<int> x{1,2,3,4,5,6,7};
// ...
vector<int>(x).swap(x); // 压缩到合适容量
vector<int>().swap(x); // 删除所有元素
真正地删除元素:std::remove并不执行删除操作

STL中的std::remove算法并不真正地从容器中删除元素。因为std::remove属于algorithm,只操作迭代器范围,不掉用容器的成员函数,所以是不可能从容器中真正删除元素的。 来看一下SGISTL中的实现(SGISTL的实现太老,没有用到std::move):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class _InputIter, class _Tp>
inline _InputIter find(_InputIter __first, _InputIter __last, const _Tp& __val)
{
while (__first != __last && !(*__first == __val))
++__first;
return __first;
}
template <class _InputIter, class _OutputIter, class _Tp>
_OutputIter remove_copy(_InputIter __first, _InputIter __last, _OutputIter __result, const _Tp& __value) {
for ( ; __first != __last; ++__first)
if (!(*__first == __value)) {
*__result = *__first;
++__result;
}
return __result;
}
template <class _ForwardIter, class _Tp>
_ForwardIter remove(_ForwardIter __first, _ForwardIter __last, const _Tp& __value) {
__first = find(__first, __last, __value);
_ForwardIter __i = __first;
return __first == __last ? __first : remove_copy(++__i, __last, __first, __value);
}

可以看到它们只是移动元素的位置,并非真正地把元素删除,只是将不该删除的元素移动到容器的首部,然后返回新的结束位置迭代器。 等于是把删除的部分移动到了元素的尾部,所以要真正地删除容器中所有匹配的元素,需要用erase-remove惯用法:

1
c.erase(std::remove(c.begin(),c.end(),value),c.end()); // 删除std::remove之后容器尾部的元素

谨防隐藏基类中的重载函数

如果基类中具有一个虚函数func但是其又重载了几个非虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A{
virtual void func(){
cout<<"A::func()"<<endl;
}
void func(int){
cout<<"A::func(int)"<<endl;
}
void func(double){
cout<<"A::func(double)"<<endl;
}
};
struct B:public A{
virtual void func(){
cout<<"B::func()"<<endl;
}
};

如果我们想要在B对象中使用非虚版本的func函数:

1
2
3
B x;
// error: too many arguments to function call, expected 0, have 1
x.func(123);

这是由于派生类在覆盖基类虚函数的时候会隐藏其他的重载函数,需要在B中显式引入:

1
2
3
4
5
6
7
struct B:public A{
virtual void func(){
cout<<"B::func()"<<endl;
}
// 将A::func的重载函数引入作用域
using A::func;
};

宏的替代

宏在预处理阶段被替换,此时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成员函数。

异常安全

  1. 析构函数、operator new、operator delete不能抛出异常
  2. swap操作不要抛出异常
  3. 首先做任何可能抛出异常的事情(但不会改变对象重要的状态),然后以不会抛出异常的操作结束。
  4. 当一个被抛出的异常从throw表达式奔向catch子句时,所经之路任何一个部分执行的函数比从执行堆栈上移除其激活记录之前,都必须清理他所控制的任何资源。
  5. 不要在代码中插入可能会提前返回的代码、调用可能会抛出异常的函数、或者插入其他一些东西从而使得函数末尾的资源释放得不到执行。

指向类成员函数指针的cv版本

如果我们具有一个类A,其中具有重载的成员函数func,而他们的区别只是该成员函数是否为const,那么在定义一个指向成员函数的指针时如何分别?

1
2
3
4
5
6
7
8
struct A{
void func()const{
std::cout<<"void func()const"<<std::endl;
}
void func(){
std::cout<<"void func()"<<std::endl;
}
};

如果我们只是创建一个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
2
3
4
5
6
const A x;
(x.*funcP)(); // ERROR!
(x.*funcConstP)(); // OK
A y;
(y.*funcConstP)(); // ERROR!
(y.*funcP)(); // OK

STL中的compare操作实现

不同于C语言中的宏,使用C++中的模板(template)和谓词(Predicates)可以很轻易的写出泛型的比较操作。 在宏定义中还要注意参数的副作用,因为宏只是简单的替换,比如:

1
2
3
4
#define MAX(a,b) a>=b?a:b;
MAX(--a,++b);
// 被替换为
--a>=++b?--a:++b;

但是这个宏的实际操作这并不是我们所期待的行为。 幸运的是,在C++中我们可以使用模板来避免这种丑陋的宏定义,而且也可以传递一个自定义的谓词来实现我们的判断行为:

1
2
3
4
5
6
7
8
9
10
11
struct Compare{
template<typename T>
bool operator()(const T& a,const T& b){
return a<b?false:true;
}
};
template<class T, class Compare>
const T& max(const T& a, const T& b, Compare comp)
{
return (comp(a, b)) ? b : a;
}

计算性构造函数

在某些情况下,可以通过创建构造函数的方式来提高成员函数的执行效率。

1
2
3
4
5
6
7
8
9
10
11
12
struct String{
String(const char* init);
const String operator+(const String& l,const String& r){
return String(l.s_,r.s_);
}
private:
String(const char* a,const char* b){
s_=new char[strlen(a)+strlen(b)+1];
strcat(strcpy(s_,a),b);
}
char *s_;
};

自身类型的using成员

怎么定义一个类的成员中能够获取到当前类类型的成员呢? 可以用下面这种写法:

1
2
3
4
5
6
template<typename T>
struct base{
using selfType=T;
};
template<typename T>
struct foo:public base<foo<T>>{};

虽然有种强行搞事的意思…

std::vector的随机访问

std::vector可以随机访问,因为其重载了[]操作符,以及有at成员函数,则通常有下面两种方式:

1
2
3
4
5
template<typename T>
void f(std::vector<T>& x){
x[0];
x.at(0);
}

以上两种随机访问方式有什么区别?

顺序容器的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
2
3
4
5
6
7
8
9
10
11
12
// at(size_type)的实现
#ifdef __STL_THROW_RANGE_ERRORS
void _M_range_check(size_type __n) const {
if (__n >= this->size())
__stl_throw_range_error("vector");
}
reference at(size_type __n)
{ _M_range_check(__n); return (*this)[__n]; }
const_reference at(size_type __n) const
{ _M_range_check(__n); return (*this)[__n]; }
#endif /* __STL_THROW_RANGE_ERRORS */
​`

再看一下operator[] (size_type)的实现:

1
2
3
// operator[](size_type)的实现
reference operator[](size_type __n) { return *(begin() + __n); }
const_reference operator[](size_type __n) const { return *(begin() + __n); }

可以看到,operator[]的随机访问并没有范围检查。 即上面的问题:

1
2
x[0];
x.at(0);

这两个的区别在于,若x不为空,则行为相同,若x为空,x.at(0)则抛出一个std::out_of_range异常(C++标准规定),而x[0]是未定义行为。

注意typedef和#define的区别

1
2
3
4
5
6
7
8
typedef int* INTPTR;
#define INTPTR2 int*
int main(int argc,char* argv[])
{
INTPTR i1,i2;
INTPTR2 i3,i4;
return 0;
}

还是直接从IR代码来看吧:

1
2
3
4
%6 = alloca i32*, align 8
%7 = alloca i32*, align 8
%8 = alloca i32*, align 8
%9 = alloca i32, align 4

注意%9不是i32*,它是一个i32的对象。 因为#define只是编译期的简单替换,所以在编译期展开的时候会变成这样:

1
2
3
4
#define INTPTR2 int*
INTPTR2 i3,i4;
// 编译期展开
int* i3,i4;

即只有i3为int*,而i4则为int

为什么const object不是编译时常量?

1
2
const int x=10;
int y[x]={0};

这里是可以的,在编译器优化下x会直接被替换为10 其中间代码如下:

1
2
3
4
5
%6 = alloca i32, align 4
%7 = alloca [10 x i32], align 16
store i32 10, i32* %6, align 4
%8 = bitcast [10 x i32]* %7 to i8*
call void @llvm.memset.p0i8.i64(i8* %8, i8 0, i64 40, i32 16, i1 false)

可以看到%7的分配时并没有使用%6,所以也并不依赖x这个对象,这个对象是编译期已知的。 但是,当我们这么写时,又如何编译期可知:

1
2
3
4
5
int x;
cin>>x;
const int y=x;
// error: variable-sized object may not be initialized
int z[y]={0};

这里是由于编译器扩展,所以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:

  • If, in the most derived object pointed (referred) to by v, v points (refers) to a public base class subobject of a C object, and if only one object of type C is derived from the subobject pointed (referred) to by v the result points (refers) to that C object.
  • Otherwise, if v points (refers) to a public base class subobject of the most derived object, and the type of the most derived object has a base class, of type C, that is unambiguous and public, the result points (refers) to the C subobject of the most derived object.
  • Otherwise, the run-time check fails.

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
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
#include <iostream>
using namespace std;
struct Shape{
virtual void draw()=0;
virtual ~Shape(){}
};
struct Roll{
virtual void roll(){cout<<"Roll:roll()"<<endl;}
virtual ~Roll(){}
};
struct Circle:public Shape,public Roll{
void draw(){
cout<<"Circle::draw"<<endl;
}
void roll(){
cout<<"Circle::roll()"<<endl;
}
~Circle()=default;
};
struct Square:public Shape{
void draw(){
cout<<"Square::draw()"<<endl;
}
~Square()=default;
};
int main(int argc,char* argv[])
{
Shape *a=new Square;
Roll *b=dynamic_cast<Roll*>(a);
if(b!=NULL){
cout<<"yes"<<endl;
}else{
cout<<"no"<<endl;
}
delete a;
return 0;
}
// output: no

面的继承层次比较简单,但是当假设我们不知道Cricle和Square的具体继承层次时,那么如何判断Square中是否存在某一基类(如Roll)? 解决的办法就是上面提到的dynamic_cast!通过dynamic_cast转换到转换到要检测的类类型的指针,如果转换成功,dynamic_cast会返回从源类型转换到目标类型的指针,如果失败会返回一个空指针(之所以不使用引用是因为要处理可能会抛出异常的潜在威胁),这种转换并非是向上或者向下转型,而是横向转型。所以我们需要对dynamic_cast返回的对象(指针)作一个判断就可以得出检测目标的继承层次中是否存在要检测的类型。

但是,我觉得这种行为的适用场景十分狭窄,在良好的类设计下几乎不必要,如果你对自己所实现的类层次感到失控,那一定是糟糕的设计。

参考文献

C/C++中的编程技巧及其概念

l-c-skill

发表于 2017-10-12 | 分类于 language

Segmentation fault段错误调试总结

Segmetation fault也叫做段错误,引发的原因有好多,这里我们只说一下段错误发生时的调试方法。

方法1:加打印printf。

这是最基本的往往也很有效的方法,在哪里Core掉就会在哪里停止打印–一目了然。同时这种方法也存在一个致命缺陷:如果恰巧Core掉的地方没加打印而程序代码又非常庞大又可能是多线程的,那查找问题等同于大海捞针。

方法2:gdb调试。

加gdb调试往往能在Core dump时抓到,甚至能抓到哪一个文件哪个类哪个函数哪一行,甚是精确。要确保GDB能抓到可用信息要做一些准备:

  • 加-g 参数,这样才会有调试信息。 我想是个程序员就应该知道吧。
  • 在Makefile 中加上 -fstack-protector 和-fstack-protector-all 信息,确保函数调用栈不丢失,当然只能是一定程度的不丢失,要完成保留住是不太可能的,但起码可以得到栈顶函数。

有了上面两点对大多数的Segmentation fault都能抓住,但是函数调用栈彻底乱掉或者在动态库so中Core而这个库编译时没有加-g参数,这些情况就gdb就无能为力了。

方法3:手动获取函数调用栈。

这种方法其实是借住两个系统函数backtrace和backtrace_symbol来获取函数调用栈的,把这两个函数放在信号处理函数中:当收到 SIGSEG时在信号处理函数中调用这两个函数打印函数调用栈,在没用GDB调试的时候这种方法可以代替gdb的一部分功能,这听起来是不是非常酷啊,来看一看实现吧:

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
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

static void SignalHandle(int sig)
{
void *array[20];
size_t size;
char **strings;
int i;
size = backtrace(array, 10);
strings = backtrace_symbols(array, size);
printf("SIGNAL ocurre %d, stack tarce:\n", sig);
printf("obtained %d stack frames.\n", size);

for (i = 0; i < size; i++)
printf("%s\n", strings);

free(strings);
printf("stack trace over!\n");
exit(0);
}

int main(int argc, char **argv)
{
signal(SIGSEGV, SignalHandle);
//...程序主体
}

当然这种方法在没有GDB时候会大显身手,经过实验就是有gdb的时候这种方法有时比gdb抓到调用栈要多一层;当然这种方法和用gdb调试一样要加-g和栈保护参数-fstack-protector 和 -fstack-protector-all。其缺点就是抓到的调用栈无效,这是什么意思呢?有时发生core dump,能定位到甚至哪一行,但是那一行根本没有明显的错误;或者追到没有调试信息的动态库里如glibc。当然这些情况大多数调试方法都无能为力,只能依靠程序员的经验了。

方法4:经验之谈。

如果我们的程序是多线程的,发生core dump用以上方法均无效,除了仔细排查代码外,还有这么一方法让我们缩小范围。

c语言全局变量那些事 “C++的数组不支持多态”?

代码执行的效率

深入理解C语言

对象的消息模型

读书笔记:对线程模型的批评

C语言的谜题

C语言函数实现的另类方法

谁说C语言很简单?

C语言下的错误处理的问题

C语言结构体里的成员数组和指针

C技巧:结构体参数转成不定参数

arithmetic-kmp

发表于 2017-10-12

https://baike.baidu.com/item/kmp%E7%AE%97%E6%B3%95/10951804?fr=aladdin

http://blog.csdn.net/yutianzuijin/article/details/11954939/

(转)从头开始写项目makefile

发表于 2017-10-10 | 分类于 makefile

1. 基本规则

一般一个稍大的linux项目会有很多个源文件组成,最终的可执行程序也是由这许多个源文件编译链接而成的。编译是把一个.c或.cpp文件编译成中间代码.o文件,链接是就使用这些中间代码文件生成可执行文件。比如在当前项目目录下有如下源文件:

1
2
3
# ls  
common.h debug.c debug.h ipc.c ipc.h main.c tags timer.c timer.h tools.c tools.h
#

以上源代码可以这样编译:

1
# gcc -o target_bin main.c debug.c ipc.c timer.c tools.c

如果之后修改了其中某一个文件(如tools.c),再执行一下上一行代码即可,但如果有成千上万个源文件这样编译肯定是不够合理的。此时我们可以按下面步骤来编译:

1
2
3
4
5
6
# gcc -c debug.c  
# gcc -c ipc.c
# gcc -c main.c
# gcc -c timer.c
# gcc -c tools.c
# gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

如果其中tools.c修改了,只需要编译该文件,再执行最后生成可执行文件的操作,也就是做如下两步操作即可:

1
2
# gcc -c tools.c  
# gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

这样做看上去应该很合理了。但是如果修改了多个文件,就很可能忘了编译某一文件,那么运行时就很有可能出错。如果是common.h文件修改了,那么包含该头文件的所有.c文件都需要重新编译,这样一来的话就更复杂更容易出错了。看来这种方法也不够好,手动处理很容易出错。那有没有一种自动化的处理方式呢?有的,那就是写一个Makefile来处理编译过程。 下面给一个简单的Makefile,在源代码目录下建一个名为Makefile的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
target_bin : main.o debug.o ipc.o timer.o tools.o  
>---gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

main.o: main.c common.h
>---gcc -c main.c

debug.o: debug.c debug.h common.h
>---gcc -c debug.c

ipc.o: ipc.c ipc.h common.h
>---gcc -c ipc.c

timer.o: timer.c timer.h common.h
>---gcc -c timer.c

tools.o: tools.c tools.h common.h
>---gcc -c tools.c

然后在命令行上执行命令:

1
2
3
4
5
6
7
8
9
10
11
# make   
gcc -c main.c
gcc -c debug.c
gcc -c ipc.c
gcc -c timer.c
gcc -c tools.c
gcc -o target_bin main.o debug.o ipc.o timer.o tools.o
#
# ls
common.h common.h~ debug.c debug.h debug.o ipc.c ipc.h ipc.o main.c main.o Makefile Makefile~ tags target_bin timer.c timer.h timer.o tools.c tools.h tools.o
#

可见在该目录下生成了.o文件以及target_bin可执行文件。现在我们只需要执行一个make命令就可以完成所有编译工作,无需像之前一样手动执行所有动作,make命令会读取当前目录下的Makefile文件然后完成编译步骤。从编译过程输出到屏幕的内容看得到执行make命令之后所做的工作,其实就是我们之前手动执行的那些命令。现在来说一下什么是Makefile? 所谓Makefile我的理解其实就是由一组组编译规则组成的文件,每条规则格式大致为:

1
2
3
target ... : prerequisites ...   
>---command
...

其中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工作过程如下:

  1. 首先查找第一条规则目标,第一条规则的目标称为缺省目标,只要缺省目标更新了就算完成任务了,其它工作都是为这个目的而做的。 该Makefile中第一条规则的目标target_bin,由于我们是第一次编译,target_bin文件还没生成,显然需要更新,但此时依赖文件main.o debug.o ipc.o timer.o tools.o都没有生成,所以需要先更新这些文件,然后才能更新target_bin。
  2. 所以make会进一步查找以这些依赖文件main.o debug.o ipc.o timer.o tools.o为目标的规则。首先找main.o,该目标也没有生成,该目标依赖文件为main.c common.h,文件存在,所以执行规则命令gcc -c main.c,生成main.o。其他target_bin所需要的依赖文件也同样操作。
  3. 最后执行gcc -o target_bin main.o debug.o ipc.o timer.o tools.o,更新target_bin。

在没有更改源代码的情况下,再次运行make:

1
2
3
# make  
make: `target_bin' is up to date.
#

得到提示目标target_bin已经是最新的了。 如果修改文件main.c之后,再运行make:

1
2
3
4
5
# vim main.c  
# make
gcc -c main.c
gcc -o target_bin main.o debug.o ipc.o timer.o tools.o
#

此时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。 总结下,执行一条规则步骤如下:

  1. 先检查它的依赖文件,如果依赖文件需要更新,则执行以该文件为目标的的规则。如果没有该规则但找到文件,那么该依赖文件不需要更新。如果没有该规则也没有该文件,则报错退出。
  2. 再检查该文件的目标,如果目标不存在或者目标存在但依赖文件修改时间比他要晚或某依赖文件已更新,那么执行该规则的命令。 由此可见,Makefile可以自动发现更新过的文件,自动重新生成目标,使用Makefile比自己手动编译比起来,不仅效率高,还减少了出错的可能性。

Makefile中有很多目标,我们可以编译其中一个指定目标,只需要在make命令后面带上目标名称即可。如果不指定编译目标的话make会编译缺省的目标,也就是第一个目标,在本文给出的Makefile第一个目标为target_bin。如果只修改了tools.c文件的话,我们可能只想看看我们的更改的源代码是否有语法错误而又不想重新编译这个工程的话可以执行如下命令:

1
2
3
# make tools.o   
gcc -c tools.c
#

编译成功,这里又引出一个问题,如果继续执行同样的命令:

1
2
3
# make tools.o  
make: `tools.o' is up to date.
#

我们先手动删掉tools.o文件再执行就可以了,怎么又是手动呢?我们要自动,要自动!!好吧,我们加一个目标来删除这些编译过程中产生的临时文件,该目标为clean。 我们在上面Makefile最后加上如下内容:

1
2
clean:  
>---rm *.o target_bin

当我们直接make命令时不会执行到该目标,因为没有被默认目标target_bin目标或以target_bin依赖文件为目标的目标包含在内。我们要执行该目标需要在make时指定目标即可。如下:

1
2
3
# make clean  
rm *.o target_bin
#

可见clean目标被执行到了,再执行make时make就会重新生成所有目标对应的文件,因为执行make clean时,那些文件被清除了。 clean目标应该存在与你的Makefile当中,它既可以方便你的二次编译,又可以保持的源文件的干净。该目标一般放在最后,不可放在最开头,否则会被当做缺省目标被执行,这很可能不是你的意愿。 最后总结一下,Makefile只是告诉了make命令如何来编译和链接程序,告诉make命令生成目标文件需要的文件,具体的编译链接工作是你的目标对应的命令在做。 给一个今天完整的makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
target_bin : main.o debug.o ipc.o timer.o tools.o  
>---gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

main.o: main.c common.h
>---gcc -c main.c

debug.o: debug.c debug.h common.h
>---gcc -c debug.c

ipc.o: ipc.c ipc.h common.h
>---gcc -c ipc.c

timer.o: timer.c timer.h common.h
>---gcc -c timer.c

tools.o: tools.c tools.h common.h
>---gcc -c tools.c

clean:
>---rm *.o target_bin

2. 隐含规则自动推导

上一节的Makefile勉强可用,但还写的比较繁琐,不够简洁。对每一个.c源文件,都需要写一个生成其对应的.o目标文件的规则,如果有几百个或上千个源文件,都手动来写,还不是很麻烦,这也不够自动化啊。 这样,我们把生成.o目标文件的规则全部删除掉,就是这样一个Makefile文件:

1
2
3
4
5
target_bin : main.o debug.o ipc.o timer.o tools.o  
>---gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

clean:
>---rm *.o target_bin

这下简洁了不少,这样也能用吗?试试看吧先,make一下:

1
2
3
4
5
6
7
8
# make  
cc -c -o main.o main.c
cc -c -o debug.o debug.c
cc -c -o ipc.o ipc.c
cc -c -o timer.o timer.c
cc -c -o tools.o tools.c
gcc -o target_bin main.o debug.o ipc.o timer.o tools.o
#

原来酱紫都可以啊!!target_bin后面那一群依赖文件怎么生成呢?不是没有生成*.o目标文件的规则了吗?再看屏幕编译输出内容:

1
2
3
4
5
cc    -c -o main.o main.c  
cc -c -o debug.o debug.c
cc -c -o ipc.o ipc.c
cc -c -o timer.o timer.c
cc -c -o tools.o tools.c

怎么长的和之前不太一样呢,尤其是前面那个cc是何物? 其实make可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个*.o文件后都写上类似的命令,因为,我们的 make 会自动推导依赖文件,并根据隐含规则自己推导命令。所以上面.o文件是由于make自动推导出的依赖文件以及命令来生成的。 下面来看看make是如何推导的。 命令make –p可以打印出很多默认变量和隐含规则。Makefile变量可以理解为C语言的宏,直接展开即可(后面会讲到)。取出我们关心的部分:

1
2
3
4
5
6
7
8
9
10
# default  
OUTPUT_OPTION = -o $@
# default
CC = cc
# default
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) –c
# Implicit Rules
%.o: %.c
# commands to execute (built-in):
>---$(COMPILE.c) $(OUTPUT_OPTION) $<

其中cc是一个符号链接,指向gcc,这就可以解释为什么我们看到的编译输出为cc,其实还是使用gcc在编译。

1
2
3
# ll /usr/bin/cc    
lrwxrwxrwx. 1 root root 3 Dec 3 2013 /usr/bin/cc -> gcc
#

变量$(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
2
main.o : main.c:  
>--- cc -c -o main.o main.c

其余几个目标也同样推导。make自动推导的功能为我们减少了不少的Makefile代码,尤其是对源文件比较多的大型工程,我们的Makefile可以不用写得那么繁琐了。 最后,今天的Makefile相对于上一节进化成这个样子了:

1
2
3
4
5
target_bin : main.o debug.o ipc.o timer.o tools.o  
>---gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

clean:
>---rm *.o target_bin

3. 变量的使用

仔细研究我们的之前Makefile发现,我们还有改进的地方,就是此处:

1
2
target_bin : main.o debug.o ipc.o timer.o tools.o  
>---gcc -o target_bin main.o debug.o ipc.o timer.o tools.o

如果增加一个源文件xx.c的话,需要在两处或多处增加xx.o文件。我们可以使用变量来解决这个问题。之前说过,Makefile的变量就像C语言的宏一样,使用时在其位置上直接展开。变量在声明时赋予初值,在引用变量时需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。 默认目标target_bin也在多处出现了,该文件也可以使用变量代替。 修改我们的Makefile如下:

1
2
3
4
5
6
7
SRC_OBJ = main.o debug.o ipc.o timer.o tools.o  
SRC_BIN = target_bin
$(SRC_BIN) : $(SRC_OBJ)
>---gcc -o $(SRC_BIN) $(SRC_OBJ)

clean:
>---rm $(SRC_OBJ) $(SRC_BIN)

这样每次有新增的文件是只需要在SRC_OBJ变量里面增加一个文件即可。要修改最终目标的名字是可以只修改变量SRC_BIN。 其实在之前还说过特殊变量:

  • $@,表示规则中的目标。
  • $<,表示规则中的第一个依赖文件。
  • $?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。 $^:,表示规则中的所有条件,组成一个列表,以空格分隔。 上一节我们看到make -p有很多自定义的变量,比如CC。其中很多变量我们可以直接使用或修改其变量值或增加值。我们的Makefile中可以使用CC(默认值为cc)、RM(默认值为rm -f)。

由此可见我们的Makefile还可以进一步修改:

1
2
3
4
5
6
SRC_OBJ = main.o debug.o ipc.o timer.o tools.o  
SRC_BIN = target_bin
$(SRC_BIN) : $(SRC_OBJ)
>---$(CC) -o $@ $^
clean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN)

这样的Makefile编译也是可用的。 但是这样的Makefile还是需要我们手动添加文件,还是不够自动化,最好增删文件都要修改Makefile。伟大的人类真是太懒了!!于是乎,他们发明了一个函数wilcard(函数后面会讲到),它可以用来获取指定目录下的所有的.c文件列表。这样的话我们可以自动获取当前目录下所有.c源文件,然后通过其他方法再得到.o文件列表,这样的话就不需要在每次增删文件时去修改Makefile了。所谓其他方法这里给出两种:

  1. 使用patsubst函数。在$(patsubst %.c,%.o,$(dir) )中,patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o。
  2. 变量值的替换。 我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。

修改后的Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
# SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))                                                                                                                                          

SRC = $(wildcard *.c)
SRC_OBJ = $(SRC:.c=.o)
SRC_BIN = target_bin

$(SRC_BIN) : $(SRC_OBJ)
>---$(CC) -o $@ $^

clean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN)

其中# 后面的内容为注释。 这样终于满足了那些懒人的想法了。可见在使用变量时,的确可以是编译变得更自动化。

其实变量的定义有三种运算符=、:=、?=、+=。

  1. =运算符可以读取到后面定义的变量。比如:
    1
    2
    3
    4
    5
    VAR = $(VAR2)  
    VAR2 = hello_make

    all:
    >---@echo =====$(VAR)=====

运行结果为:

1
2
3
#  
=====hello_make=====
#

但是这种定义可能会导致并非我们意愿的事发生,并不是很符合C语言的编程习惯。

  1. :=运算符在遇到变量定义时立即展开。
    1
    2
    3
    4
    5
    VAR := $(VAR2)                                                                                         
    VAR2 = hello_make

    all:
    >---@echo =====$(VAR)=====

运行结果为:

1
2
3
#  
==========
#
  1. ?=运算符在复制之前先做判断变量是否已经存在。例如var1 ?= $(var2)的意思是:如果var1没有定义过,那么?=相当于=,如果var1先前已经定义了,则什么也不做,不会给var重新赋值。
  2. +=运算符是给变了追加值。如果变量还没有定义过就直接用+=赋值,那么+=相当于=

如何使用这几个运算符要看实际情况,有时一个大的工程可能有许多Makefile组成,变量可能在多个Makefile中都在使用,这时可能使用+=比较好。使用:=有时可能比要好。 有时在编译程序时,我们需要编译器给出警告,或加入调试信息,或告知编译器优化可执行文件。编译时C编译器的选项CFLAGS使用的较多,默认没有提供值,我们可以给该变量赋值。有时我们还需要使用链接器选项LFLAGS告诉链接器链接时需要的库文件。可能我们还需要给出包含头文件的路径,因为头文件很可能和源文件不再同一目录。所以,我们今天的Makefile加上部分注释又更新了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# A commonMakefile for c programs, version 1.0  
# Copyright (C)2014 shallnew \at 163 \dot com

CFLAGS += -g -Wall-Werror -O2
CPPFLAGS += -I.-I./inc
LDFLAGS +=-lpthread

# SRC_OBJ =$(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES =$(wildcard *.c)
SRC_OBJ =$(SRC_FILES:.c=.o)
SRC_BIN =target_bin

$(SRC_BIN) :$(SRC_OBJ)
>---$(CC) -o $@$^ $(LDFLAGS)

clean:
>---$(RM)$(SRC_OBJ) $(SRC_BIN)

编译:

1
2
3
4
5
6
7
8
# make  
cc -g -Wall-Werror -O2 -I. -I./inc -c -o debug.odebug.c
cc -g -Wall-Werror -O2 -I. -I./inc -c -o ipc.oipc.c
cc -g -Wall-Werror -O2 -I. -I./inc -c -o main.omain.c
cc -g -Wall-Werror -O2 -I. -I./inc -c -o timer.otimer.c
cc -g -Wall-Werror -O2 -I. -I./inc -c -o tools.otools.c
cc -o target_bindebug.o ipc.o main.o timer.o tools.o -lpthread
#

可见我们的预编译选项,编译选项都用到了,之前我们说过make的使用隐含规则自动推导:

1
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) –c

其中变量CFLAGS 和 CPPFLAGS均是我们给出的,变量$(TARGET_ARCH)未给,所以在编译输出可以看到-c前面有2个空,最早未给变量是有四个空。 目前给出的Makefile基本上可以适用于那些源代码全部在同一目录下的简单项目,并且基本上在增删文件时不需要再去手动修改Makefile代码。在新的一个项目只需要把该Makefile拷贝到源代码目录下,再修改一下你需要编译的可执行文件名称以及你需要的编译连接选项即可。 后面章节将会讲到如何写多目录源代码工程下的Makefile。 最后,今天的最终Makefile是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# A commonMakefile for c programs, version 1.0  
# Copyright (C)2014 shallnew \at 163 \dot com

CFLAGS += -g -Wall-Werror -O2
CPPFLAGS += -I.-I./inc
LDFLAGS +=-lpthread

# SRC_OBJ =$(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES =$(wildcard *.c)
SRC_OBJ =$(SRC_FILES:.c=.o)
SRC_BIN =target_bin

$(SRC_BIN) :$(SRC_OBJ)
>---$(CC) -o $@$^ $(LDFLAGS)

clean:
>---$(RM)$(SRC_OBJ) $(SRC_BIN)

3. 伪目标

一般情况下,Makefile都会有一个clean目标,用于清除编译过程中产生的二进制文件。我们在第一节的Makefile就用到了这个 clean目标,该目标没有任何依赖文件,并且该目标对应的命令执行后不会生产clean文件。 像这种特点目标,它的规则所定义的命令不是去创建文件,而仅仅通过make指定目标来执行一些特定系统命令或其依赖为目标的规则(如all),称为伪目标。 一个Makefile一般都不会只有一个伪目标,如果按Makefile的“潜规则”以及其约定俗成的名字来说的话,在较大的项目的Makefile中比较常用的为目标有这些:

  • all:执行主要的编译工作,通常用作缺省目标,放在最前面。
  • Install:执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安装目录。
  • clean:删除编译生成的二进制文件。
  • distclean:删除除源文件之外的所有中间生成文件,如配置文件,文档等。
  • tags:为vim等编辑器生成tags文件。
  • help:打印当前Makefile的帮助信息,比如有哪些目标可以有make指定去执行。 等。

make处理Makefile时,首先读取所有规则,建立关系依赖图。然后从缺省目标(第一个目标)或指定的目标开始执行。像clean,tags这样的目标一般不会作为缺省目标,也不会跟缺省目标有任何依赖关系,所以 make 无法生成它的依赖关系和决定它是否要执行。所以要执行这样的目标时,必须要显示的指定make该目标。就像前面我们清楚便已产生的中间二进制文件一样,需要显示执行命令:make clean。 伪目标也可以作为默认目标(如all),并且可以为其指定依赖文件。 我们先将version 1.0的Makefile完善下,我们可以加入帮助信息,tags等功能。

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
# A common Makefile for c programs, version 1.1  

# Copyright (C) 2014 shallnew \at 163 \dot com

CFLAGS += -g -Wall -Werror -O2
CPPFLAGS += -I. -I./inc
LDFLAGS += -lpthread

# SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES = $(wildcard *.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_BIN = target_bin

all : $(SRC_BIN)

$(SRC_BIN) : $(SRC_OBJ)
>---$(CC) -o $@ $^ $(LDFLAGS)

obj : $(SRC_OBJ)

tags:
>---ctags -R

help:
>---@echo "===============A common Makefile for cprograms=============="
>---@echo "Copyright (C) 2014 liuy0711 \at 163 \dotcom"
>---@echo "The following targets are support:"
>---@echo
>---@echo " all - (==make) compile and link"
>---@echo " obj - just compile, without link"
>---@echo " clean - clean target"
>---@echo " distclean - clean target and otherinformation"
>---@echo " tags - create ctags for vim editor"
>---@echo " help - print help information"
>---@echo
>---@echo "To make a target, do 'make [target]'"
>---@echo "========================= Version 1.1======================="

# clean target
clean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN) $(SRC_BIN).exe

distclean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN) $(SRC_BIN).exe tags *~

make会把执行的命令打印在屏幕上,如果我们不想把命令打印在屏幕上,只显示命令结果时,直接在命令前面加上符号“@”就可以实现。如上面help目标一样,只显示命令结果。一般我们会在make时都会输出“Compiling xxx.c…”,不输出编译时的命令。我们在后面写Makefile时可以模仿。 如果当前目录下存在一个和伪目标同名的文件时(如clean),此时如果执行命令make clean后出现如下结果:

1
2
3
4
# touch clean  
# make clean
make: `clean' is up to date.
#

这是因为clean文件没有依赖文件,make认为目标clean是最新的不会去执行规则对应的命令。为了解决这个问题,我们可以明确地将该目标声明为伪目标。将一个目标声明为伪目标需要将它作为特殊目标.PHONY”的依赖。如下:

1
.PHONY : clean

这条规则写在clean:规则的后面也行,也能起到声明clean是伪目标的作用 这样修改一下之前Makefile,将所有伪目标都作为.PHONY的依赖:

1
.PHONY : all obj tag help clean disclean

这样在当前目录下存在文件clean时执行:

1
2
3
# make clean  
rm -f debug.o ipc.o main.o timer.o tools.o target_bin target_bin.exe
#

发现问题解决。 最后,给出今天最终的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
# A common Makefile for c programs, version 1.1                                                                                                                                            
# Copyright (C) 2014 shallnew \at 163 \dot com

CFLAGS += -g -Wall -Werror -O2
CPPFLAGS += -I. -I./inc
LDFLAGS += -lpthread

# SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES = $(wildcard *.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_BIN = target_bin

all : $(SRC_BIN)

$(SRC_BIN) : $(SRC_OBJ)
>---$(CC) -o $@ $^ $(LDFLAGS)

obj : $(SRC_OBJ)

tag:
>---ctags -R

help:
>---@echo "===============A common Makefile for cprograms=============="
>---@echo "Copyright (C) 2014 liuy0711 \at 163 \dotcom"
>---@echo "The following targets are support:"
>---@echo
>---@echo " all - (==make) compile and link"
>---@echo " obj - just compile, without link"
>---@echo " clean - clean target"
>---@echo " distclean - clean target and other information"
>---@echo " tags - create ctags for vim editor"
>---@echo " help - print help information"
>---@echo
>---@echo "To make a target, do 'make [target]'"
>---@echo "========================= Version 1.1======================="

# clean target
clean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN) $(SRC_BIN).exe

distclean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN) $(SRC_BIN).exe tags *~

.PHONY : all obj tag help clean disclean

5. 嵌套执行

在大一些的项目里面,所有源代码不会只放在同一个目录,一般各个功能模块的源代码都是分开的,各自放在各自目录下,并且头文件和.c源文件也会有各自的目录,这样便于项目代码的维护。这样我们可以在每个功能模块目录下都写一个Makefile,各自Makefile处理各自功能的编译链接工作,这样我们就不必把所有功能的编译链接都放在同一个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
.  

├── include
│ ├── common.h
│ ├── ipc
│ │ └── ipc.h
│ └── tools
│ ├── base64.h
│ ├── md5.h
│ └── tools.h
├── Makefile
├── src
│ ├── ipc
│ │ ├── inc
│ │ ├── Makefile
│ │ └── src
│ │ └── ipc.c
│ ├── main
│ │ ├── inc
│ │ ├── Makefile
│ │ └── src
│ │ ├── main.c
│ │ └── main.c~
│ └── tools
│ ├── inc
│ ├── Makefile
│ └── src
│ ├── base64.c
│ ├── md5.c
│ └── tools.c
└── tags

13 directories, 16 files

这样组织项目源码要比之前合理一些,那这样怎么来写Makefile呢?我们可以在每个目录下写一个Makefile,通过最顶层的Makefile一层一层的向下嵌套执行各层Makefile。那么我们最顶层的Makefile简单点的话可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
# top Makefile for xxx  

all :
>---$(MAKE) -C src

tags:
>---ctags -R

clean :
>---$(MAKE) -C src clean

.PHONY : all clean tags

命令:

1
>---$(MAKE) -C src

就是进入src目录继续执行该目录下的Makefile。然后src目录下的Makefile在使用同样的方法进入下一级目录tools、main、ipc,再执行该目录下的Makefile。其实这样有些麻烦,我们可以直接从顶层目录进入最后的目录执行make。再加入一些伪目标完善下,我们的顶层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
# Top Makefile for C program  

# Copyright (C) 2014 shallnew \at 163 \dot com

all :
>---$(MAKE) -C src/ipc
>---$(MAKE) -C src/tools
>---$(MAKE) -C src/main

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 ======================="

obj:
>---$(MAKE) -C src/ipc obj
>---$(MAKE) -C src/tools obj
>---$(MAKE) -C src/main obj

clean :
>---$(MAKE) -C src/ipc clean
>---$(MAKE) -C src/tools clean
>---$(MAKE) -C src/main clean

distclean:
>---$(MAKE) -C src/ipc distclean
>---$(MAKE) -C src/tools distclean
>---$(MAKE) -C src/main distclean

.PHONY : all clean distclean tags help

当我们这样组织源代码时,最下面层次的Makefile怎么写呢?肯定不可以将我们上一节的Makefile(version 1.1)直接拷贝到功能模块目录下,需要稍作修改。不能所有的模块都最终生成各自的可执行文件吧,我们目前是一个工程,所以最后只会生成一个可执行程序。我们这样做,让主模块目录生成可执行文件,其他模块目录生成静态库文件,主模块链接时要用其他模块编译产生的库文件来生成最终的程序。将上一节Makefile稍作修改得出编译库文件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
# A Makefile to generate archive file  
# Copyright (C) 2014 shallnew \at 163 \dot com


CFLAGS += -g -Wall -Werror -O2
CPPFLAGS += -I. -I./inc -I../../include

# SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES = $(wildcard src/*.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_LIB = libtools.a

all : $(SRC_LIB)

$(SRC_LIB) : $(SRC_OBJ)
>---$(AR) rcs $@ $^
>---cp $@ ../../libs

obj : $(SRC_OBJ)

# clean target
clean:
>---$(RM) $(SRC_OBJ) $(SRC_LIB)

distclean:
>---$(RM) $(SRC_OBJ) $(SRC_LIB) tags *~

.PHONY : all obj clean disclean

====================

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
# A Makefile to generate executive file                                                                                                                                                     
# Copyright (C) 2014 shallnew \at 163 \dot com

CFLAGS += -g -Wall -Werror -O2
CPPFLAGS += -I. -I./inc -I../../include
LDFLAGS += -lpthread -L../../libs -ltools -lipc


# SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES = $(wildcard src/*.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_BIN = target_bin

all : $(SRC_BIN)

$(SRC_BIN) : $(SRC_OBJ)
>---$(CC) -o $@ $^ $(LDFLAGS)

obj : $(SRC_OBJ)

# clean target
clean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN) $(SRC_BIN).exe

distclean:
>---$(RM) $(SRC_OBJ) $(SRC_BIN) $(SRC_BIN).exe tags*~

.PHONY : all obj clean disclean

最后在顶层执行:

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
# make clean  

make -C src/ipc clean
make[1]: Entering directory`/home/Myprojects/example_make/version-3.0/src/ipc'
rm -f src/ipc.o libipc.a
make[1]: Leaving directory`/home/Myprojects/example_make/version-3.0/src/ipc'
make -C src/tools clean
make[1]: Entering directory `/home/Myprojects/example_make/version-3.0/src/tools'
rm -f src/base64.o src/md5.o src/tools.o libtools.a
make[1]: Leaving directory`/home/Myprojects/example_make/version-3.0/src/tools'
make -C src/main clean
make[1]: Entering directory`/home/Myprojects/example_make/version-3.0/src/main'
rm -f src/main.o target_bin target_bin.exe
make[1]: Leaving directory`/home/Myprojects/example_make/version-3.0/src/main'
# make
make -C src/ipc
make[1]: Entering directory`/home/Myprojects/example_make/version-3.0/src/ipc'
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/ipc.osrc/ipc.c
ar rcs libipc.a src/ipc.o
cp libipc.a ../../libs
make[1]: Leaving directory `/home/Myprojects/example_make/version-3.0/src/ipc'
make -C src/tools
make[1]: Entering directory`/home/Myprojects/example_make/version-3.0/src/tools'
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/base64.osrc/base64.c
cc -g -Wall -Werror -O2 -I. -I./inc -I../../include -c -o src/md5.o src/md5.c
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/tools.osrc/tools.c
ar rcs libtools.a src/base64.o src/md5.o src/tools.o
cp libtools.a ../../libs
make[1]: Leaving directory`/home/Myprojects/example_make/version-3.0/src/tools'
make -C src/main
make[1]: Entering directory`/home/Myprojects/example_make/version-3.0/src/main'
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/main.osrc/main.c
cc -o target_bin src/main.o -lpthread -L../../libs -ltools-lipc
make[1]: Leaving directory`/home/Myprojects/example_make/version-3.0/src/main'
#

最后生成了可执行程序文件。这样的话一个工程的各个模块就变得独立出来了,不但源码分开了,而且各自有各自的Makefile,并且各个功能模块是可独立编译的。 我们发现顶层Makefile还有可以改进的地方,就是在进入下一层目录是要重复写多次,如下:

1
2
3
>---$(MAKE) -C src/ipc  
>---$(MAKE) -C src/tools
>---$(MAKE) -C src/main

每增加一个目录都要在多个伪目标里面加入一行,这样不够自动化啊,于是我们想到shell的循环语 句,我们可以在每条规则的命令处使用for循环。如下:

1
2
3
4
5
6
7
DIR = src  
SUBDIRS = $(shell ls $(DIR))

all :
>---@for subdir in $(SUBDIRS); \
>---do $(MAKE) -C $(DIR)/$$subdir; \
>---done

这样懒人有可以高兴很久了。不过还有问题: 上面for循环会依次进入系统命令ls列出的目录,但我们对每个目录的make顺序可能有要求,在该项目当中,main目录下的Makefile必须最后执行,因为最终的链接需要其他目录编译生成的库文件,否则会执行失败。并且在当前的Makefile中,当子目录执行make出现错误时,make不会退出。在最终执行失败的情况下,我们很难根据错误的提示定位出具体是是那个目录下的Makefile出现错误。这给问题定位造成了很大的困难。为了避免这样的问题,在命令执行错误后make退出。 所以将刚才的Makefile修改为如下

1
2
3
4
5
6
7
DIR = src  
SUBDIRS = $(shell ls $(DIR))

all :
>---@for subdir in $(SUBDIRS); \
>---do $(MAKE) -C $(DIR)/$$subdir || exit 1; \
>---done

这样在执行出错时立马退出,但这样还是没有解决问题,编译错误还是会出现。那怎么解决呢? 我们可以通过增加规则来限制make执行顺序,这样就要用到伪目标,对每一个模块我们都为他写一条规则,每个模块名称是目标,最后需要执行的模块目标又是其他模块的目标,这样就限制了make顺序。在执行到最后需要执行的目标时,发现存在依赖,于是先更新依赖的目标,这样就不会出错了。并且这样的话,我们还可以对指定模块进行编译,比如我只修改了tools模块,我只想看看我修改的这个模块代码是否可以编译通过,我可以在编译时这样:

1
2
3
4
5
6
7
8
9
10
# make tools  
make -C src/tools
make[1]: Entering directory`/home/Myprojects/example_make/version-2.1/src/tools'
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/base64.o src/base64.c
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/md5.osrc/md5.c
cc -g -Wall -Werror -O2 -I. -I./inc-I../../include -c -o src/tools.osrc/tools.c
ar rcs libtools.a src/base64.o src/md5.o src/tools.o
cp libtools.a ../../libs
make[1]: Leaving directory`/home/Myprojects/example_make/version-2.1/src/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

6.参数传递、条件判断、include

在多个Makefile嵌套调用时,有时我们需要传递一些参数给下一层Makefile。比如我们在顶层Makefile里面定义的打开调试信息变量DEBUG_SYMBOLS,我们希望在进入子目录执行子Makefile时该变量仍然有效,这是需要将该变量传递给子Makefile,那怎么传递呢?这里有两种方法:

  1. 在上层Makefile中使用”export”关键字对需要传递的变量进行声明。比如:

    1
    2
    DEBUG_SYMBOLS = TRUE  
    export DEBUG_SYMBOLS

    当不希望将一个变量传递给子 make 时,可以使用指示符 “unexport”来声明这个变量。 export一般用法是在定义变量的同时对它进行声明。如下:

    1
    export DEBUG_SYMBOLS = TRUE
  1. 在命令行上指定变量。比如:
    1
    $(MAKE) -C xxx DEBUG_SYMBOLS = TRUE

这样在进入子目录xxx执行make时该变量也有效。

像编程语言一样,Makefile也有自己的条件语句。条件语句可以根据一个变量值来控制make的执行逻辑。比较常用的条件语句是ifeq –else-endif、ifneq-else-endif、ifdef-else-endif。 ifeq关键字用来判断参数是否相等。 比如判断是否生成调试信息可以这么用:

1
2
3
4
5
ifeq ($(DEBUG_SYMBOLS), TRUE)  
>---CFLAGS += -g -Wall -Werror -O0
else
>---CFLAGS += -Wall -Werror -O2
endif

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
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
# Top Makefile for C program                                                                                                                                                               
# Copyright (C) 2014 shallnew \at 163 \dot com

export DEBUG_SYMBOLS = TRUE

DIR = src
MODULES = $(shell ls $(DIR))
# MODULES = ipc main tools

all : $(MODULES)

$(MODULES):
>---$(MAKE) -C $(DIR)/$@

main:tools ipc

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 " 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.2 ======================="

.PHONY : all clean distclean tags help

目前我们顶层目录下的目录树为:

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
.  
├── include
│ ├── common.h
│ ├── ipc
│ │ └── ipc.h
│ └── tools
│ ├── base64.h
│ ├── md5.h
│ └── tools.h
├── libs
├── Makefile
├── Makefile.rule
└── src
├── ipc
│ ├──inc
│ ├──Makefile
│ └──src
│ └── ipc.c
├── main
│ ├──inc
│ ├──Makefile
│ └──src
│ ├── main.c
│ └── main.c~
└── tools
├── inc
├── Makefile
└── src
├── base64.c
├── md5.c
└── tools.c

14 directories, 16 files

每个子模块下的Makefile删除规则后修改为如下:

1
2
3
4
5
6
7
8
9
10
11
SRC_BASE = ../..  

CFLAGS +=
CPPFLAGS += -I. -I./inc -I$(SRC_BASE)/include

# SRC_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))
SRC_FILES = $(wildcard src/*.c)
SRC_OBJ = $(SRC_FILES:.c=.o)
SRC_LIB = libtools.a

include $(SRC_BASE)/Makefile.rule

而处于顶层目录下的Makefile.rule专门处理各模块编译链接时需要的规则。内容如下:

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
# Copyright (C) 2014 shallnew \at 163 \dot com                                                                                                                                             

ifeq ($(DEBUG_SYMBOLS), TRUE)
>---CFLAGS += -g -Wall -Werror -O0
else
>---CFLAGS += -Wall -Werror -O2
endif

all : $(SRC_BIN) $(SRC_LIB)

ifneq ($(SRC_BIN),)
$(SRC_BIN) : $(SRC_OBJ)
>---$(CC) -o $@ $^ $(LDFLAGS)
endif

ifneq ($(SRC_LIB),)
$(SRC_LIB) : $(SRC_OBJ)
>---$(AR) rcs $@ $^
>---cp $@ $(SRC_BASE)/libs
endif

# clean target
clean:
>---$(RM) $(SRC_OBJ) $(SRC_LIB) $(SRC_BIN)$(SRC_BIN).exe

distclean:
>---$(RM) $(SRC_OBJ) $(SRC_LIB) $(SRC_BIN)$(SRC_BIN).exe $(SRC_BASE)/libs/* $(SRC_BASE)/tags *~

.PHONY : all clean disclean
~

我们将Makefile.rule放在顶层有可能会一不小心在命令行上面执行了该Makefile,如下:

1
2
3
# make -f Makefile.rule  
make: Nothing tobe done for `all'.
#

由于我们没有定义变量$(SRC_BIN)和$(SRC_LIB),伪目标all没有任何依赖,所以编译是无法成功的。这里我们我们应该禁止直接执行该Makefile。 在make里面有这样一个变量:MAKELEVEL,它在多级调用的 make 执行过程中。变量代表了调用的深度。在 make 一级级的执行过程中变量MAKELEVEL的值不断的发生变化,通过它的值我们可以了解当前make 递归调用的深度。顶层的MAKELEVEL的值为“0” 、下一级时为“1” 、再下一级为“2”…….,所以我们希望一个子目录的Makefile必须被上层 make 调用才可以执行,而不允许直接执行,我们可以判断变量MAKELEVEL来控制。所以我们这一节最终的Makefile.rule为:

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
# Copyright (C)2014 shallnew \at 163 \dot com  

ifeq ($(DEBUG_SYMBOLS),TRUE)
>---CFLAGS +=-g -Wall -Werror -O0
else
>---CFLAGS +=-Wall -Werror -O2
endif

ifeq($(MAKELEVEL), 0)
all : msg
else
all : $(SRC_BIN)$(SRC_LIB)
endif

ifneq ($(SRC_BIN),)
$(SRC_BIN) :$(SRC_OBJ)
>---$(CC) -o $@$^ $(LDFLAGS)
endif

ifneq($(SRC_LIB),)
$(SRC_LIB) :$(SRC_OBJ)
>---$(AR) rcs$@ $^
>---cp $@$(SRC_BASE)/libs
endif

msg:
>---@echo"You cannot directily execute this Makefile! This Makefile should calledby toplevel Makefile."

# clean target
clean:
>---$(RM)$(SRC_OBJ) $(SRC_LIB) $(SRC_BIN) $(SRC_BIN).exe

distclean:
>---$(RM)$(SRC_OBJ) $(SRC_LIB) $(SRC_BIN) $(SRC_BIN).exe $(SRC_BASE)/libs/*$(SRC_BASE)/tags *~

.PHONY : all cleandisclean

此时再直接执行该Makefile:

1
2
3
# make -f Makefile.rule  
You cannot directily execute this Makefile! This Makefile should called by toplevel Makefile.
#

7. 统一目标输出目录

上一节我们把规则单独提取出来,方便了Makefile的维护,每个模块只需要给出关于自己的一些变量,然后再使用统一的规则Makefile。这一节我们继续改进我们的Makefile,到目前为止我们的Makefile编译链接输出的目标都在源文件同目录下或模块Makefile同一目录下,当一个项目大了之后,这样会显得很乱,寻找编译输出的文件也比较困难。既然Makefile本身就是按照我们的的规则来编译链接程序,那么我们就可以指定其编译链接目标的目录,这样,我们可以清楚输出文件的地方,并且在清除已编译的目标时直接删除指定目录即可,不需要一层一层的进入源代码目录进行删除,这样又提高了效率。

既然要统一目标输出目录,那么该目录就需要存在,所以我们可以增加一条规则来创建这些目录,包括创建可执行文件的目录、链接库文件的目录以及.o文件的目录。并且目录还可以通过条件判断根据是否产生调试信息来区分开相应的目标文件。一般一个工程的顶层目录下都会有一个build目录来存放编译的目标文件结果,目前我的工程目录下通过Makefile创建的目录build的目录树如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
build/            //build根目录  
├── unix //unix平台项目下不带调试信息输出目录
│ ├── bin //存放可执行文件目录
│ ├── lib //存放可文件目录
│ └── obj //存放.o文件目录,该目录下将每个模块生成的.o文件各自的目录下面
│ ├── ipc
│ ├── main
│ └── tools
└── unix_dbg ////unix平台项目下带调试信息输出目录
├── bin
├── lib
└── obj
├── ipc
├── main
└── tools

14 directories, 0 files
```
以上目录中bin和lib目录在顶层Makefile中创建,obj及其下面模块子目录在各模块的Makefile里面创建。
顶层Makefile创建目录如下:

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
2
3
我们在all目标里面增加了其依赖目标BUILDDIR,该目标对应的规则为创建bin目录和lib目录。这样每次编译之前都会创建目录。

各模块内部Makefile创建生成.O文件的目录,如上目录树所示。类似于顶层Makefile,各模块内部Makefile需要根据平台、编译调试信息、以及模块名称来生成需要的目录名称,然后再增加创建该目录的规则。因为每个模块都会做这些处理,所以我们将这部分写在规则Makefile(Makefile.rule)里面,如下:

……

define a root build directory base on the platform

if without a SRC_BASE defined, just use local src directory

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
2
3
4
5
6
由于我们是开启了调试信息,所以创建了unix_dbg目录,并且该目录下创建了bin、lib、obj目录及其模块目录,但我们没有发现有文件存放在里面。

到目前为止,这一节仅仅讲述如何创建统一的目标文件存放目录,但是要想将编译生成的目标文件自动生成到这些目录还没有完成。其实我们只需要给目标加上路径即可,但还是有一些详细的地方需要处理,具体的我们会在下一节中讲到,这一节暂不给出最后的Makefile。

### 8. 模式规则
上一节讲到目录创建成功,目标文件没有生产到对应目录下,这里我们先给目标文件加上对应目录,这样的话产生对应的目标文件会直接生成到对应目录。我们先给库文件目标和可执行文件目标加上路径,如下:

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_OBJ = $(patsubst %.c, %.o, $(wildcard *.c))

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
2
这里用到函数patsubst、notdir,关于函数会在后面讲到。这样.o文件作为目标生成之后就会生成到相应目录里面了。
此时再编译:

make

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
2
发现出错了,并且是在生成目标文件ipc.o时没有成功,查看build目录树也没有生成.o文件。为什么会生成失败呢?
我们没有给出生成.o目标的规则,之前可以生成是因为make有通过隐含规则来自动推导的能力(这个之前有讲到,链接过去)。在我们没有修改之前,生成.o通过隐含规则来完成:

%.o: %.c

commands to execute (built-in):

—$(COMPILE.c) $(OUTPUT_OPTION) $<

1
该模式规则中目标文件是$(OBJDIR)/%.o,那么现在有了符合生成我们需要的.o文件的规则了,编译一下:

make

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
2
3
4
5
6
发现还是不对,不是已经增加了模式规则了吗,为何还是没有生成.o文件。
我们这里先说说静态模式规则:

一个规则中可以有多个目标,规则所定义的命令对所有的目标有效。一个具有多目标的规则相当于多个规则。 规则的命令对不同的目标的执行效果不同, 因为在规则的命令中可能使用了自动化变量 `“$@”` 。 多目标规则意味着所有的目标具有相同的依赖文件。多目标通常用在以下两种情况:虽然在多目标的规则中, 可以根据不同的目标使用不同的命令 (在命令行中使用自动化变量 `“$@”` )。但是, 多目标的规则并不能做到根据目标文件自动改变依赖文件 (像上边例子中使用自动化变量“$@”改变规则的命令一样) 。需要实现这个目的是,要用到make的静态模式。

静态模式规则是这样一个规则:规则存在多个目标, 并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。静态模式规则比多目标规则更通用, 它不需要多个目标具有相同的依赖。但是静态模式规则中的依赖文件必须是相类似的而不是完全相同的。静态模式规则语法如下:

<targets …>: : <prereq-patterns …>

….

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
2
3
4
5
6
7
8
9
注:该示例与其相关描述摘抄于互联网,描述很不错,估计比我讲的详细)

那静态模式规则和普通的模式规则(非静态模式规则)有什么去区别呢?两者都是用目标模式和依赖模式来构建目标的规则中的文件依赖关系,两者不同的地方是 make 在执行时使用它们的时机。
静态模式规则只能用在规则中明确指出的那些文件的重建过程中。不能用在除此之外的任何文件的重建过程中,并且它对指定的每一个目标来说是唯一的。如果一个目标存在于两个规则,并且这两个规则都定义了命令, make 执行时就会提示错误。
非静态模式规则可被用在任何和它相匹配的目标上,当一个目标文件同时符合多个目标模式时,make将会把第一个目标匹配的模式规则作为重建它的规则。

那有没有想过如果我们指定了模式规则后,那还有隐含规则呢,那怎么选择执行哪一个模式规则呢?Makefile中明确指定的模式规则会覆盖隐含模式规则。就是说如果在Makefile中出现了一个对目标文件合适可用的模式规则,那么make就不会再为这个目标文件寻找其它隐含规则,而直接使用在Makefile中出现的这个规则。在使用时,明确规则永远优先于隐含规则。

我们继续说之前的那个问题,我们定义了模式规则后还是没有生成.o文件,我们现在将其改为静态规则再试试就看,如下:

$(SRC_OBJ) : $(OBJDIR)/%.o : %.c

—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

1
执行后:

make

make[1]: Entering directory /home/Myprojects/example_make/version-2.9/src/ipc' make[1]: *** No rule to make targetipc.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
2
3
4
5
6
7
8
9
发现提示没有文件ipc.c,这说明没有生成.o的原因是没有.c文件,我很好奇的是为何使用非静态模式为何不提示呢?(还没搞懂,再研究研究,知道的可以给个提示哈~~)

缺少依赖文件,为何没有*.c文件,仔细想想我们的.o文件没有和.c文件在同一目录。在我们工程中,将源代码和二进制文件(.o 文件和可执行文件)安排在不同的目录来进行区分管理。这种情况下,我们可以使用 make 提供的目录搜索依赖文件功能。该功能在下一节讲述,这一节说的够多了,有点累了。可惜最终还是没有给出一个可用的Makefile,在下一节将会给出。

### 9. 目标搜索
在一个较大的工程中,一般会将源代码和二进制文件(.o 文件和可执行文件)安排在不同的目录来进行区分管理。这种情况下,我们可以使用 make 提供的目录搜索依赖文件功能(在指定的若干个目录下自动搜索依赖文件)。在Makefile中,使用依赖文件的目录搜索功能。当工程的目录结构发生变化后,就可以做到不更改 Makefile的规则,只更改依赖文件的搜索目录。

在我们上一节出现的问题当中,我们将.c文件统一放在src目录下,没有和Makefile目录在同一目录下,因此没有办法寻找到.o文件的依赖文件。make程序有一个特殊的变量VPATH,该变量可以指定依赖文件的搜索路径,当规则的依赖文件在当前目录不存在时,make 会在此变量所指定的目录下去寻找这些依赖文件。通常我们都是用此变量来指定规则的依赖文件的搜索路径。
定义变量 “VPATH”时,使用空格或者冒号(:)将多个需要搜索的目录分开。make搜索目录的顺序是按照变量“VPATH”定义中的目录顺序进行的,当前目录永远是第一搜索目录。例如如下定义

VPATH += ./src

1
2
3
指定了依赖搜索目录为当前目录下的src目录,我们可以在Makefile.rules里面添加给VPATH变量赋值,而在包含该Makefile.rules之前给出当前模块.c文件所在目录。

其实我们也可以直接指定依赖文件的路径,这样也是可以的,如下:

$(SRC_OBJ) : $(OBJDIR)/%.o : $(MOD_SRC_DIR)/%.c

—$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

1
2
3
4
5
6
7
8
9
10
11
12

但是这样在我们更改了工程目录结构之后,对应的依赖文件没有在同一目录下,又变得麻烦了,所以还不如直接给VPATH变量赋值,我们只需要指定源码所在的目录即可。

其实我们还有另外一种搜索文件路径方法:使用vpath关键字(注意不是VPATH变量), 它和VPATH类似,但是它可以为不同类型的文件(由文件名区分)指定不同的搜索目录。使用方法有三种:
1. vpath PATTERN DIRECTORIES
为所有符合模式“PATTERN”的文件指定搜索目录“DIRECTORIES” 。多个目录使用空格或者冒号(:)分开。
2. vpath PATTERN
清除之前为符合模式“PATTERN”的文件设置的搜索路径。
3. vpath
清除所有已被设置的文件搜索路径。

vapth 使用方法中的“PATTERN”需要包含模式字符“%”;例如上面的定义:

VPATH += ./src

1
可以写为:

vpath %.c ./src

1
现在给一个我们的Makefile.rules:

Copyright (C) 2014 shallnew \at 163 \dot com

if without a platform defined, give value “unknow” to PLATFORM

ifndef PLATFORM

—PLATFORM = unknow
endif

define a root build directory base on the platform

if without a SRC_BASE defined, just use local src directory

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

update compilation flags base on “DEBUG_SYMBOLS”

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 target

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
2
3
4
5
6
7
8
9
10
11
12
13

### 10. make内嵌函数及make命令显示
这一节我们讲一下make的函数,在之前的章节已经讲到了几个函数:wildcard、patsubst、notdir、shell等。一般函数的调用格式如下:
`$(funcname arguments)`
或
`$(funcname arguments)`
其中funcname是需要调用函数的函数名称,应该是make内嵌函数;arguments是函数参数,参数和函数名之间使用空格分割,如果存在多个参数时,参数之间使用逗号“,”分开。函数调用以“$”开头,使用成对的圆括号或花括号把函数名和参数括起,一般使用圆括号。
下面来看一下常用的一些函数:

1. 获取匹配模式文件名函数—wildcard 。
用法:`$(wildcard PATTERN)`
该函数会列出当前目录下所有符合模式“PATTERN”格式的文件名。返回空格分割的、存在当前目录下的所有符合模式“PATTERN”的文件名。
例如:

SRC_FILES = $(wildcard src/*.c)

1
2
3
4
5
6
7
8
返回src目录下所有.c文件列表。
2. 字符串替换函数—subst。
用法:`$(subst FROM,TO,TEXT)`
该函数把字串“TEXT”中的“FROM”字符替换为“TO”,返回替换后的新字符串。
3. 模式替换函数—patsubst。
用法:`$(patsubst PATTERN,REPLACEMENT,TEXT)`
该函数搜索“TEXT”中以空格分开的单词,将符合模式“TATTERN”替换为“REPLACEMENT” 。参数“PATTERN”中可以使用模式通配符“%”,来代表一个单词中的若干字符。如果参数“REPLACEMENT”中也包含一个“%” ,那么“REPLACEMENT”中的“%”将是“TATTERN”中的那个“%”所代表的字符串。
例如:

SRC_OBJ = $(patsubst %.c, %.o, $(SRC_FILES))

1
2
3
4
5
将SRC_FILES中所有.c文件替换为.o返回给变量SRC_OBJ。
此函数功能类似之前讲过的变量替换,http://blog.csdn.net/shallnet/article/details/37529935
变量替换格式是“$(var:a=b)”或“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。
例如我们存在一个代表所有.c 文件的变量。定义为“src_files = a.c b.c c.c” 。
为了得到这些.c文件所对应的.o源文件。如下两种使用可以得到同一种结果:

$(objects:.c=.o)
$(patsubst %.c,%.o,$( src_files))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
4.    过滤函数—filter。
用法:$(filter PATTERN…,TEXT)
该函数过滤掉字串“TEXT”中所有不符合模式“PATTERN”的单词,保留所有符合此模式的单词。可以使用多个模式。模式中一般需要包含模式字符“%” 。存在多个模式时,模式表达式之间使用空格分割。返回空格分割的“TEXT”字串中所有符合模式“PATTERN”的字串。
5. 反过滤函数—filter-out。
用法:`$(filter-out PATTERN...,TEXT)`
和“filter”函数实现的功能相反。过滤掉字串“TEXT”中所有符合模式“PATTERN” 的单词, 保留所有不符合此模式的单词。 可以有多个模式。存在多个模式时,模式表达式之间使用空格分割。
6. 取目录函数—dir。
用法:`$(dir NAMES…)`
从文件名序列“NAMES…”中取出各个文件名的目录部分。文件名的目录部分就是包含在文件名中的最后一个斜线`( “/” )` (包括斜线)之前的部分。返回空格分割的文件名序列“NAMES…”中每一个文件的目录部分。如果文件名中没有斜线,认为此文件为当前目录`( “./” )`下的文件。
7. 取文件名函数——notdir。
用法:`$(notdir NAMES…)`
从文件名序列“NAMES…”中取出非目录部分。目录部分是指最后一个斜线`( “/” )` (包括斜线)之前的部分。删除所有文件名中的目录部分,只保留非目录部分。文件名序列“NAMES…”中每一个文件的非目录部分。
8. 取后缀函数—suffix。
用法:`$(suffix NAMES…) `
函数从文件名序列“NAMES…”中取出各个文件名的后缀。后缀是文件名中最后一个以点“.”开始的(包含点号)部分,如果文件名中不包含一个点号,则为空。 返回以空格分割的文件名序列“NAMES…”中每一个文件的后缀序列。
9. 取前缀函数—basename。
用法:`$(basename NAMES…)`
从文件名序列“NAMES…”中取出各个文件名的前缀部分(点号之后的部分) 。前缀部分指的是文件名中最后一个点号之前的部分。 返回空格分割的文件名序列“NAMES…”中各个文件的前缀序列。如果文件没有前缀,则返回空字串。

这里仅仅讲到一些常用的函数,还有一些函数没有讲到,用到的时候可以去翻翻makefile手册。

通常情况下make在编译时会打印出当前正在执行的命令,当编译链接选项很长时,会输出很多东西在屏幕上,如果我 不想再屏幕上看到很多东西,我们可以在命令前面加上@,这样命令就不会输出到屏幕了。我们这样尝试修改下:

make

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
2
3
发现只有进入目录和退出目录的显示,这样很难知道目前编译过程。其实我们可以在规则命令处加入一行类似打印:
`@echo "do something......"`
这样可以输出目前正在做的事,又不会输出正在执行命令。现在将规则修改下如下:

$(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

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
2
其中目录切换的输出仍然很多,我们可以将其关闭,这需要使用到make的参数,在make -C是指定--no-print-
directory参数。我们将顶层目录下Makefile规则修改如下:

$(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
编译输出:

make

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…

make clean

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】
1…121314…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次