C++函数(重点)

  • 函数和数组
  • 函数和结构
  • 函数和指针
  • 函数和string
  • 函数递归

函数定义:

typename functionnname(parameterlist){
     statements;
     return value;
}

函数原型:如果函数定义在函数调用之后,必须在函数调用前进行函数原型声明,函数原型描述了函数到编译器的接口,将函数返回值类型、参数类型和数量告诉编译器。 函数原型是一条语句,就是函数定义中的函数头,此外,在原型的参数中不要求提供变量名,有类型列表就足够了。

函数参数和按值传递:

函数和数组:

1、输入为数组:

定义:
int sum_array (int array[],int n)
实际情况是array是一个指针,在函数内部,array可以看成是数组
调用:
sum_array(cooke,4) 其中cooke是数组名

2、使用指针处理数组:

数组名==指针


定义:
int sum_array (int* array,int n)
调用:
sum_array(cooke,4) 其中cooke是数组名

3、使用 输入为数组区间的函数:

 int sum_array (const int* start,const int* end ) 
调用:

sum_array(cooke,cooke+3) 其中cooke是数组名

指针和const:

将const用于指针有很微妙的地方,第一种方法是让指针指向一个常量对象,这样可以防止该指针来修改所指向的值,第二种方法是将指针本身声明为常量,防止改变指针指向的位置。

但pt的声明并不意味着它指向的值实际上就是一个常量,而只意味着队医pt来说,是一个常量,因此可以通过修改age来 改变age值。

注意:c++禁止将const地址付给非const的指针,除非使用强制准换。

函数和二维数组:

定义函数: 注意这个[4]中的数字必不可少
int sum_array (int array[][4],int n)
调用:

sum_array (cooke,4)

or
定义:array是一个由4个指向int的指针组成的数组
int sum_array (int (*array)[4],int n)

函数和c风格字符串

表示字符串方法有三种:

char数组 、用括号引起的字符串常量、被设置为字符串的地址的char指针。

字符串作为参数来传递,实际传递的是字符串的第一个字符的地址,因此,需要在函数中将表示字符串的形参声明为char * 类型

int 函数名(const char * str,char ch)

函数返回c风格字符串的函数

在函数内部 声明一个字符串指针 : char * x = char [n+1],然后return x

函数和结构:

定义结构:

struct tracal{
    int x;
    char y;
}
声明函数原型:
tracal sum(tracal x)
也可以传递结构的地址:
tracal sum(tracal *x)

函数和string 对象 array对象

函数递归:

函数指针:

函数也有地址,可以通过指针获取到函数的地址,也可以通过函数指针调用函数

  1. 获取函数的地址:获取 函数地址很简单 ,只要 使用函数名(不加参数即可),例如think()是一个函数名,那么函数地址就是think。

如果要将函数作为参数进行传递,必须是传递函数名;

  1. 声明函数指针:
对于函数:double pam(int)
声明一个函数指针:
double (*pf) (int)
和函数声明类似,不过是将函数名改为指针(加括号),

(*pf) (int)意味着是一个 指向函数的指针

  1. 使用函数指针调用函数

double (*pf) (int) 定义之后

令: pf = pam(函数名)

调用:x=(*pf)(5)

typeof 别名

c++ 分支语句和逻辑运算符

  • if语句
  • 逻辑运算符 && ! ||
  • cctype字符函数库
  • 条件运算符:?:
  • switch语句
  • continue和break
  • 读取数字的循环
  • 基本文件输入输出

1、 if语句

if (test_condition){ body } 如果test_condition为true,那么就执行 body语句。

2、 if else 语句

if(test_condition)
    {body}
else
    {body}

3、 if else if else 语句

if(test_condition)
    {body}
else if
    {body}

else
    {body}

易错点:赋值运算符=和比较运算符==,在test_condition中使用==表达等于

逻辑表达式

1、逻辑或 ||

2、逻辑与 &&

A&&B 只有A和B都为true时,表达式才为true\

可以用来表示范围 A<20 && A> 30

3、逻辑非 !

!A

注意: 逻辑运算符 &&和||优先级都低于关系运算符,!运算符则大于所有关系运算符和算数运算符。

此外可以使用 and or not表示

字符函数库 cctype #include <cctype>

该头文件声明了一组用于对单个字符进行分类和转换的函数。

条件运算符 ?:

表达式1?表达式2:表达式3

a>1?b=2:b=4 如果a>1,那么令b=2,否则b=4

switch语句 case中的value必须是int整数

switch(interger-expression){
    case value1: 
             body;
             break;
    case value2: 
             body;
             break;
    .............
    case valuen: 
             body;
             break;
default:body
}

break和continue

break用于switch语句,跳出switch;continue用于循环语句,用于跳出本次循环。

写文本文件

读取文件

c++ 循环和关系表达式

1、for循环

for循环主要完成以下部分:1、设置初始值 2、判断循环是否继续进行 3、执行循环操作 4、更新用于测试的值

for(初始值;测试条件;更新测试值)//初始值可以 自定义变量   int x = 0

for循环中更新值:i++ i– 也可以选择不同步长 i = i+2 i=i+4

2、递增运算符 ++ 和 递减运算符 —

a++ :先使用a,在a自增 ++a :先a自加一,在使用a

for循环语句块:

字符串的比较:

string类字符串比较:

3、while循环

while(循环条件){
body
}

4、类型别名

方法一:

方法二:

5、do while 循环

基于范围的循环 (C++11 新增)

用于数组或者容器类,如vector 和 array。

for ( int x : array) { body }

如果要在循环体对 x进行修改:for (int &x : arrayname) 需要使用引用。

循环和文本输入:

1、使用原始cin输入:

补救:使用cin.get(char):会读取空格,并赋给char

文件尾条件 EOF

二维数组

初始化:

使用二维数组: maxtemps[1][2]

c++ 复合类型

  • 1、数组

数组声明: typename arrayname[arraysize]

或者 在声明时候赋值 int x[2]={1,2};

如果只对数组的一部分进行初始化,则编译器会把其他元素设置为0,因此:

long tio[5]={10};

如果初始化时方括号内【】为空,编译器自动计算元素个数:

short totals[ ]={1,2,3}

注意:arraysize指定元素的数目 ,必须是整型常数或者const值,具体来说,arraysize不能是变量。当然可以使用new运算符来避开这种限制。

访问数组元素:arrayname[i] i从0开始,到size-1

c++11 初始化数组时,省略等号 doouble x[2] {1,2};

可以不再大括号中包含任何内容 doouble x[2] {} ,这将会把所有元素设为0

c++的标准模板库提供了一种数组替代品 -模板类vector,c++11新增了array。

  • 字符串

c++处理字符串方式有两种 一种来自c语言,另一种基于string类库的方法。

c语言风格字符串性质:以空字符结尾 ,空字符 \0,其ascii码为0.

char dog[3] ={‘b’,’e’,’\0′} //字符串

char cat[3] ={‘a’,’v’,’d’} //字符数组

char bird[11] = “mr. cheepes” //注意“”里不用显示的包括\0,但隐式的包括空字符,所以数组大小必须比实际长度大于一个以上。

使用键盘输入字符串时,将自动加上空字符

注意字符串常量(” “)不能与字符常量(’ ‘)互换。”s”表示字符串,是由’s’和’\0’组成,而’s’表示单个字符。

字符串输入:

cin>>name; cin使用空白(空格、制表符、换行符)来确定字符串结束的位置,因此cin在读取 字符串时只能读取一个单词,然后将其放到数组name中。

为了解决上面的问题,将整个一行字符作为输入:istream中cin提供了类成员函数:getline()和get(),读取输入,直到遇到换行符后才停止。区别 getline() 将丢弃换行符,get将保留换行符在输入队列中。

cin.getline(name,size)

name:存储的数组名,size:读取的字符数,如果size=20,那么最多读取19个字符,且不hi存储最后的换行符,余下空间用于自动存储\0空字符。

cin.get( name,size )

混合输入字符串和数字

  • string类

包含头文件 <string>,string类位于std命名空间中,必须使用using编译指令或者使用std::string引用它。

定义: string str1; string str2=”sssssss”;

赋值: cin>>str1;

显示:cout<<str1

通过访问数组的方法访问string ,str1[3]

string和字符数组区别:可以将 string对象声明为简单变量,而不是数组。声明时候不用指定大小。

c++11 初始化string : string one={“dddfse”}; string two{“sdhfhs”}

string赋值、拼接和附加 string str2 =str1 string str3 =str1+str2

c中ctring头文件中的函数 strcpy(chaar1,charr2) //copy charr2 to charr1 strcat(charr1,charr2) 拼接charr2到charr1中

string的输入输出 cin cout,当读取一行时,使用的方法不同:

数组字符串: cin.getline(charr,20)

string: getline(cin,str1)

  • 结构体

创建该类型变量 : inflatable hat;

可以将结构作为参数传递给函数,也可以让函数返回一个结构,可以使用 = 将一个结构赋给同类型的结构。

可以在定义结构体同时创建结构变量,只需要将变量名放在定义 结构的括号后面。

结构数组

  • 共用体 union

共用体是一种数据格式,能够存储不同的数据类型 ,但只能同时同时存储其中的一种类型。

声明 和struct类似

  • 枚举 enum ,可以替代 const,还允许定义新类型

enum spectrum {red,orange,yellow,green,blue,violet,indigo,ultraviolet};

默认情况下,将整数值赋给枚举变量 ,第一个枚举值为0,以此类推。当然也可以显式指定整数来覆盖默认值。

枚举只有 赋值运算,没有算数运算。

如果使用 整数:需要强制类型转换 枚举名(整数)

设置枚举值

  • 指针、数组

地址运算符 &变量 获得该变量的地址

指针:用于存储值的地址,使用*运算符,可以得到该地址处的值,*被称为间接值运算符,即 x是一个地址,*x时该地址的数值

可以在命名的同时赋值:

int *p

声明一个指针:

int *p or int* p or int*p

可以在声明的同时初始化: int * p= &x

注意:初始化的是 p,而不是*p

new运算符

delete 释放内存

int *p = new int

delete p

注意:只能用delete删除new分配的内存

使用 new来新建动态数组

指针小结

  • 数组名是一个指针,指向的是数组array[0]
  • 将&用于数组名时候,获得的是整个数组的内存地址,因此需要取 &array[0]作为数组指针的地址

指针和字符串

C++处理字符串有两种方式,即:指针方式和数组方式

数组方式:char a[] = “HelloWorld”;
指针方式:const char* s = “HelloWorld”; const可以忽略
接下来详细讲解一下字符串指针

首先,为什么字符串可以直接赋值给指针,即char* s = “HelloWorld”不会报错,不应该是把字符串的地址赋值给指针吗?

原因:这里的双引号做了3件事:

1.申请了空间(在常量区),存放了字符串

  1. 在字符串尾加上了’/0′
    3.返回地址
    为什么字符串指针的指针名输出字符串内容而不是地址?

字符串指针的指针名代表字符串的首地址,但输出字符串指针名时输出的却是完整字符串,如下:

char* s = "HelloWorld";
cout<<s<<endl; //s是字符串的首地址,但却输出HelloWorld
cout<<*s<<endl;  //输出H
cout<<*(s+1)<<endl;  //输出e,s+1是第二个字符的地址
    cout <<static_cast<void *>(s) << endl; //此时输出的才是字符串地址

原因是C++标准库中I/O类对<<操作符重载,在遇到字符型指针时会将其当作字符串名来处理,输出指针所指的字符串。既然这样,那么我们就别让它知道那是字符型指针,所以得用到强制类型转换,用static_cast把字符串指针转换成无类型指针

字符串指针指向的地址可以修改,但所指向的字符串内容不能修改,因为字符串常量是不能改变的

char* s = "HelloWorld";
s="abcd"; //合法
cout<<*(s+1)<<endl;
*(s+1)='d'; //不合法,这里虽然没报错,但这一句实际下一句并未执行
cout<<s<<endl; //未执行

字符串指针数组:

char *p[6]={"ABCD","EFGH","IJKL","MNOP"};
int i;
for(i=0;i<4;i++) 
    cout<<p[i]<<endl;  //输出每个字符串,实际上p[i]为第i个字符串的首地址
for(i=0;i<4;i++) 
    cout<<*p[i];  //输出每个字符串第一个字符AEIM
cout<<endl;
for(i=0;i<4;i++) 

cout<<*(p[i]+1); //输出每个字符串第二个字符BFJN

C++中使用char*定义字符串,不能改变字符串内的字符的内容,但却可以把另外一个字符串(新地址)赋值给它,即p1是一个char型指针变量,其值(指向)可以改变;此时,若指向的新地址为字符串数组的地址,则可更改字符串中的内容

使用new创建动态结构:

类型组合

模板类 vector 和 array

c++ 数据类型

本文主要关于数据类型。面向对象编程的本质就是设计并扩展自己的的数据类型。

2024年 5月
 12345
6789101112
13141516171819
20212223242526
2728293031  

首先了解c++的内置数据类型 :基本类型和复合类型

基本类型:整形和浮点型 复合类型:数组、指针、字符串、结构 存储数据的方法:变量

  • 简单变量

变量命名规则:

如果想用多个单词组成一个名称,通常使用下划线字符将单词分开,如 my_onions,或者 从第二个单词开始将每个单词的第一个字母大写:myEyeTooth

  • 整型

不含小数的数字 0,-3 ,100。不同的整型使用不同的内存来存储整数。有符号和无符号类型分别表示正负数和正数

short int long longlong 通过不同数目的位存储值:(都是有符号数)

short 至少16位 :short x (short== short int)

int 至少与short一样长

long 至少32位,且至少与int一样长 (long == long int)

lonng long 之少64位,且至少与long一样长

sizeof 运算符,获得变量的所占字节,对于类型名 int等使用时,需要加括号: sizeof (int),如果是对于变量,可加可不加。

#include<iostream>
using  namespace std;
int main(){
    int x_collec =2;
    cout<<"x_collec is"<<sizeof(x_collec)<< endl;
}

初始化:

int year =2022 //如果知道变量初始值,建议定义时候赋初值。

c++11 初始化方法:将大括号用于单值变量,采用这种方法时候,=可以去掉

int x={3} or int x{3}

大括号中不含值默认为0 int z{}

头文件climits

climits定义了符号常量来并表示类型的限制: int n =INT_MAX;

  • 无符号类型

要创建无符号类型,只需要使用unsigned 来修改变量声明。

unsigned   short  x  
unsigned   int  x
unsigned   long  x       
  • char 类型 (也是整型)

char用于存储字符(字母和数字) char x =”M” ,实际上,计算机中存储的是对应的字符编码77,可以将x =x+1,char值位78,对应N,可以通过 (int)x强制转换为78

有些字符不能通过键盘直接输入,比如换行符不能用回车,因此,有了下面的转义字符:

char 占8bit,unsigned char 表示范围0-255, signed char 表示范围-128~127

c++11新增 char16_t char32_t, char16_t 无符号16位, char32_t 32位有符号数,使用前缀u表示 char16_t 类型的字符常量和字符串常量, 使用前缀U表示 char32_t 类型的字符常量和字符串常量 : char16_t ch=u’q’;

  • bool类型

布尔值 true or false,将非 0值解释为true,将0解释为false。字面值true和false都可以通过提升转换(不用显式强制转换)为 int类型,true转换为1,false转换为0。

  • const限定符

常量被初始化后就不能修改了 const int year =2022

const type name =value

浮点数

能够表示 带小数部分的数字

书写浮点数:

1、标准写法 12.34 22.3 0.12 8.0

2、E表示法 3.45E6 指的是3.45与1000000相乘结果,E6指的是10的6次方,6是指数,指数可以是正数也可以是负数。E可以写成e。

  • 浮点类型

三种:float 32位 double 64位 long double 128位,浮点数有精度限制。

float 只能保证6位精确位,double保证13位精确度。

cout所属的ostream类有一个类成员函数,能够精确的控制输出格式-字段宽度、小数位数、采用小数格式还是E格式等。后面会给出实现。

在程序中使用 浮点常量时候,默认会认为是double型,如果要指定类型,在常量后加后缀:

1.23f —-float型

1.23L —–long double

1.23 —-默认double 类型

c++ 算数运算符

加、减、乘、除、求模

除法:如果两个整数相除,结果会是一个整数(小数部分直接舍去),如果两个数中 有一个或两个是浮点数,则小数部分会被保留。(因为系统会将不同操作数进行自动准换成相同的类型)

类型转换

1、初始化和赋值进行的转换

比如赋值时 double x = 3.14f 将一个float型付给double ,如果将double付给float变量,可能会导致降低精度。int x =3.14f 最终x=3(直接丢弃小数部分)

0赋值给bool,会转换为false,非0值会变为true

2、算数运算时

变量提升:在计算表达式时c++将 bool、char、unsigned char 、signed char 和short 转换为int。

3、传递参数时转换

4、强制类型转换

首先要明确一点强制转换不会修改变量本身 ,而是创建一个新的、指定类型的值。

以下两种方法都可以:

(long) x 或者 long (x)

(typename) value type (value)

c++还引入了强制类型转换运算符: static _cast<typename> (value)

C++中的auto声明

c++11新增了auto,让编译器能够根据初始值类型推断变量类型。

typedef

C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。

typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写
https://pixabay.com/photos/nature-winter-tree-season-outdoors-6891549/

c++ 入门

本科大一的时候学习过c++,但因为后来大部分项目都是用python,所以基本上都还给老师了,但其实回过头发现,很多python开源库都是用c++写的,像opencv,因此很有必要去在回顾一下子c++的基本概念。

2024年 5月
 12345
6789101112
13141516171819
20212223242526
2728293031  
  1. c++注释

以双斜杠开头: //这是一行注释,c++也能识别c注释,c注释包括在符号/* */之间,可以跨越多行。

#include <iostream>  
int main (){  
#c++ 例子  
    usinng namespace std;  
    cout<<"hello world"  
    cout<<endl;  
    return 0  
}  
  1. 预处理器和iostream

#include <iostream>

  1. 头文件和命名空间

如果使用iostream,而不是iostream.h,则应使用下面的名称空间编译指令来使iostream中的定义对程序可用: using namespace std;

命名空间作用:假如两个封装好的库,都有名为cout的函数,那么在调用cout时,编译器不知道是哪个函数,因此可以把某个库中函数定义到一个命名空间,就可以通过 std:cout (命名空间:函数名)调用,此外,这样写比较麻烦,还可以使用using,而不必使用std前缀 : using namespace std ;使得std中所有名称可用。在大型工程中,一般使用:using std:cout; using std:cin;单独定义所需的函数

  1. 输入输出

cout<<“hello” 和 cin<<a cout 还可以拼接 cout<<“s”<<“v”<<endl;

endl 是一个特殊的c++符号,表示换行,此外还可以使用c中的\n换行符 cout<<“hello \n”

  1. 声明语句和变量

int carrots; 这条语句声明了需要的内存和内存单元名称.为什么需要声明变量:如果不显示的声明,那么当我们在多次使用 carrots 变量时候,如果中间有个写错了 carrot ,系统不会报错,而是认为这是一个新的变量。

变量赋值 a=3

  1. 函数

type functionname(argumentlist){ statements }

函数头: type functionname(argumentlist) ,函数中可以使用using编译指令,起作用范围为函数内部。

如果using 放置在函数定义之前,文件中所有的函数都可以使用std中的元素,using放在特定函数中,则该函数能使用

linux_makefile_cmake

https://seisman.github.io/how-to-write-makefile

https://cmake.org/

Makefile:规定编译的顺序规则 make就是一个gcc/g++的调度器

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。

make命令执行时,需要一个名为makefile的文件,以告诉make命令需要怎么样的去编译和链接程序。

CMake是一种跨平台编译工具,比make更为高级,使用起来要方便得多。CMake主要是编写CMakeLists.txt文件,然后用cmake命令将CMakeLists.txt文件转化为make所需要的makefile文件,最后用make命令编译源码生成可执行程序或共享库(so(shared object)).它的作用和qt的qmake是相似的。

CMake 有什么用:

GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。CMake 就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等。在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

  1. 写 CMake 配置文件 CMakeLists.txt 。
  2. 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile(ccmake 和 cmake 的区别在于前者提供了一个交互式的界面)。其中, PATH 是 CMakeLists.txt 所在的目录。
  3. 使用 make 命令进行编译。

重点:gcc和make区别

gcc是编译器 而make不是 make是依赖于Makefile来编译多个源文件的工具 在Makefile里同样是用gcc(或者别的编译器)来编译程序.

gcc是编译一个文件,make是编译多个源文件的工程文件的工具。
make是一个命令工具,是一个解释makefile中指令的命令工具。

make就是一个gcc/g++的调度器,通过读入一个文件(默认文件名为Makefile或者makefile),执行一组以gcc/g++为主的shell命令序列。输入文件主要用来记录文件之间的依赖关系和命令执行顺序。
gcc是编译工具;
make是定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译;也就是说make是调用gcc的。

Makefile的编写规则

commmond每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX声明。

makefile教程

makefile教程

编写 Makefile的文件:
target ... : prerequisites ...

[tab]

command … … 或者: targets : prerequisites ; command command … 执行:make 目标target名 target 可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。 prerequisites 生成该target所依赖的文件和/或target command(任意的命令)每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX声明。 该target要执行的命令(任意的shell命令) command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以 Tab 键开头,如果和prerequisites在一行,那么可以用分号做为分隔。(见上) prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。这个在前面已经讲过了。 如果命令太长,你可以使用反斜杠( \ )作为换行符。make对一行上有多少个字符没有限制。规则告诉make两件事,文件的依赖关系和如何生成目标文件。 一般来说,make会以UNIX的标准Shell,也就是 /bin/sh 来执行命令。

例如:

Makefile的文件:
a.txt: b.txt c.txt

[tab]

cat b.txt c.txt > a.txt 构建命令: $ make a.txt

一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象,比如上文的 a.txt 。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。

除了文件名,目标还可以是某个操作的名字,这称为”伪目标”(phony target)。

clean:
      rm *.o
上面代码的目标是clean,它不是文件名,而是一个操作的名字,属于"伪目标 ",作用是删除对象文件。
$ make  clean

井号(#)在Makefile中表示注释

make命令

执行时,需要一个makefile文件,以告诉make命令需要怎么样的去编译和链接程序。

首先,我们用一个示例来说明makefile的书写规则,以便给大家一个感性认识。这个示例来源于gnu 的make使用手册,在这个示例中,我们的工程有8个c文件,和3个头文件,我们要写一个makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。
只要我们的makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自动编译所需要的文件和链接目标程序。

edit : main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
    cc -o edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h buffer.h
    cc -c display.c
insert.o : insert.c defs.h buffer.h
    cc -c insert.c
search.o : search.c defs.h buffer.h
    cc -c search.c
files.o : files.c defs.h buffer.h command.h
    cc -c files.c
utils.o : utils.c defs.h
    cc -c utils.c
clean :
    rm edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

反斜杠( \ )是换行符的意思。这样比较便于makefile的阅读。我们可以把这个内容保存在名字为“makefile”或“Makefile”的文件中,然后在该目录下直接输入命令 make 就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下 make clean 就可以了。

在这个makefile中,目标文件(target)包含:执行文件edit和中间目标文件( *.o ),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。

这里要说明一点的是, clean 不是一个文件,它只不过是一个动作名字,有点像c语言中的label一样,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个label的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

回声

正常情况下,make会打印每条命令,然后再执行,这就叫做回声(echoing)。

test:
    # 这是测试
执行上面的规则,会得到下面的结果。


$ make test
# 这是测试

模式匹配 通配符%

Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是 %。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。

%.o: %.c

等同于下面的写法。

f1.o: f1.c
f2.o: f2.c

使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。

通配符

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 […] 。比如, *.o 表示所有后缀名为o的文件。

clean:
        rm -f *.o

变量 变量需要放在 $( )

Makefile 允许使用等号自定义变量。

txt = Hello World
test:
    @echo $(txt)

上面代码中,变量 txt 等于 Hello World。调用时,变量需要放在 $( ) 之中。

调用Shell变量,需要在美元符号前,再加一个美元符号,这是因为Make命令会对美元符号转义。

有时,变量的值可能指向另一个变量。

v1 = $(v2)
上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow


VARIABLE = value
# 在执行时扩展,允许递归扩展。

VARIABLE := value
# 在定义时扩展。

VARIABLE ?= value
# 只有在该变量为空时才设置值。

VARIABLE += value
# 将值追加到变量的尾端。

内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册。

output:
    $(CC) -o output input.c

自动变量(Automatic Variables)

Make命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。

(1)$@
$@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的 $@ 就指代foo。
.txt b.txt: 
    touch $@
等同于下面的写法。
a.txt:
    touch a.txt
b.txt:
    touch b.txt
(2)$<

$< 指代第一个前置条件。比如,规则为 t: p1 p2,那么$< 就指代p1。
a.txt: b.txt c.txt
    cp $< $@
等同于下面的写法。
a.txt: b.txt c.txt
    cp b.txt a.txt  
(3)$?

$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$?就指代p2。

(4)$^

$^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2 。

(5)$*

$* 指代匹配符 % 匹配的部分, 比如% 匹配 f1.txt 中的f1 ,$* 就表示 f1。

(6)$(@D) 和 $(@F)

$(@D) 和 $(@F) 分别指向 $@ 的目录名和文件名。比如,$@是 src/input.c,那么$(@D) 的值为 src ,$(@F) 的值为 input.c。
(7)$(<D) 和 $(<F)

$(<D) 和 $(<F) 分别指向 $< 的目录名和文件名。

所有的自动变量清单,请看手册。下面是自动变量的一个例子。
dest/%.txt: src/%.txt
    @[ -d dest ] || mkdir dest
    cp $< $@
上面代码将 src 目录下的 txt 文件,拷贝到 dest 目录下。首先判断 dest 目录是否存在,如果不存在就新建,然后,$< 指代前置文件(src/%.txt), $@ 指代目标文件(dest/%.txt)。

判断和循环

Makefile使用 Bash 语法,完成判断和循环。

ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif
上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

(1)shell 函数

shell 函数用来执行 shell 命令

srcfiles := $(shell echo src/{00..99}.txt)
(2)wildcard 函数

wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。

srcfiles := $(wildcard src/*.txt)
(3)subst 函数

subst 函数用来文本替换,格式如下。

$(subst from,to,text)
下面的例子将字符串"feet on the street"替换成"fEEt on the strEEt"。

$(subst ee,EE,feet on the street)
下面是一个稍微复杂的例子。

comma:= ,
empty:=
# space变量用两个空变量作为标识符,当中是一个空格
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now `a,b,c'.
(4)patsubst函数

patsubst 函数用于模式匹配的替换,格式如下。

$(patsubst pattern,replacement,text)
下面的例子将文件名"x.c.c bar.c",替换成"x.c.o bar.o"。


$(patsubst %.c,%.o,x.c.c bar.c)
(5)替换后缀名

替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则。它实际上patsubst函数的一种简写形式。

min: $(OUTPUT:.js=.min.js)
上面代码的意思是,将变量OUTPUT中的后缀名 .js 全部替换成 .min.js 。

函数

Makefile 还可以使用函数,格式如下。

$(function arguments)
# 或者
${function arguments}
Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。

多个文件目录下Makefile的写法:!有用的实例

1.有用的网址:

https://seisman.github.io/how-to-write-makefile/conditionals.html

2.对于多个文件都在同一目录下:

  测试程序在同一个文件中,共有func.h、func.c、main.c三个文件,Makefile写法如下所示:

makefile文件:
##-wall 使gcc对源文件的代码有问题的地方发出警告
CC = gcc
CFLAGS = -g -Wall

main:main.o func.o
    $(CC)  main.o func.o -o main
main.o:main.c
    $(CC) $(CFLAGS)  -c main.c -o main.o
func.o:func.c
    $(CC) $(CFLAGS) -c func.c -o func.o
clean:
    rm -rf *.o

执行:

make 目标

3、通用模板

  实际当中程序文件比较大,这时候对文件进行分类,分为头文件、源文件、目标文件、可执行文件。也就是说通常将文件按照文件类型放在不同的目录当中,这个时候的Makefile需要统一管理这些文件,将生产的目标文件放在目标目录下,可执行文件放到可执行目录下。测试程序如下图所示:

# innclude目录:.h头文件 
# src: .c源文件
# obj:  生成的可执行文件main
# bin: .o文件(编译过程的中间文件)
DIR_INC = ./include  
DIR_SRC = ./src
DIR_OBJ = ./obj
DIR_BIN = ./bin

SRC = $(wildcard ${DIR_SRC}/*.c)
OBJ = $(patsubst %.c,${DIR_OBJ}/%.o,$(notdir ${SRC}))

TARGET = main

BIN_TARGET = ${DIR_BIN}/${TARGET}

CC = gcc
CFLAGS = -g -Wall -I${DIR_INC}

${BIN_TARGET}:${OBJ}
    $(CC) $(OBJ)  -o $@

${DIR_OBJ}/%.o:${DIR_SRC}/%.c
    $(CC) $(CFLAGS) -c  $< -o $@
.PHONY:clean
clean:
    find ${DIR_OBJ} -name *.o -exec rm -rf {}

解释如下:

(1)Makefile中的 符号 $@, $^, $< 的意思:
  $@ 表示目标文件
  $^ 表示所有的依赖文件
  $< 表示第一个依赖文件
  $? 表示比目标还要新的依赖文件列表

(2)wildcard、notdir、patsubst的意思:

  wildcard : 扩展通配符
  notdir : 去除路径
  patsubst :替换通配符

SRC = $(wildcard *.c)

等于指定编译当前目录下所有.c文件,如果还有子目录,比如子目录为inc,则再增加一个wildcard函数,象这样:

SRC = $(wildcard .c) $(wildcard inc/.c)

(3)gcc -I -L -l的区别:

   gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld

   上面这句表示在编译hello.c时-I /home/hello/include表示将/home/hello/include目录作为第一个寻找头文件的目录,

   寻找的顺序是:/home/hello/include–>/usr/include–>/usr/local/include

  -L /home/hello/lib表示将/home/hello/lib目录作为第一个寻找库文件的目录,

   寻找的顺序是:/home/hello/lib–>/lib–>/usr/lib–>/usr/local/lib

   -lworld表示在上面的lib的路径中寻找libworld.so动态库文件(如果gcc编译选项中加入了“-static”表示寻找libworld.a静态库文件)

autoconf和automake工具

autoconf和automake两个工具来帮助我们自动地生成符合自由软件惯例的Makefile

https://thebigdoc.readthedocs.io/en/latest/auto-make-conf.html

步骤:

create project
touch NEWS README ChangeLog AUTHORS
autoscan
configure.scan ==> configure.in/configure.ac
aclocal
autoheader(可选,生成config.h.in)
Makefile.am(根据源码目录可能需要多个)
libtoolize –automake –copy –force(如果configure.ac中使用了libtool)
automake –add-missing
autoconf
./configure && make && make install

安装:

 sudo apt-get install autoconf

例子:
https://yuchen112358.github.io/2016/04/25/auto-tool/

https://www.laruence.com/2009/11/18/1154.html

打印当前工作目录 pwd 命令

Linux中用 pwd 命令来查看”当前工作目录“的完整路径。 简单得说,每当你在终端进行操作时,你都会有一个当前工作目录。

在不太确定当前位置时,就会使用pwd来判定当前目录在文件系统内的确切位置。

pwd

如果目录是链接时:

格式:pwd -P  显示出实际路径,而非使用连接(link)路径。

今天这个部分有点难🚹