指针

简介

指针是什么? 首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。

这里对于我们有JS经验的人来说,应该不难理解。就跟JS中的复杂数据类型一样, 比如声明一个数组时,其实执行了两件事,一是在堆中存储了数据,二是声明了一个变量指向这个堆中的数据,这个变量就是指针。

字符表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char表示一个指向字符的指针,float*表示一个指向float类型的值的指针。

int* intPtr;

上面示例声明了一个变量intPtr,它是一个指针,指向的内存地址存放的是一个整数。 星号*可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。

int   *intPtr;
int * intPtr;
int*  intPtr;

这种写法有一个地方需要注意,如果同一行声明两个指针变量,那么需要写成下面这样。

// 正确
int * foo, * bar;

// 错误
int* foo, bar;

上面示例中,第二行的执行结果是,foo是整数指针变量,而bar是整数变量,即*只对第一个变量生效。 一个指针指向的可能还是指针,这时就要用两个星号**表示。

int** foo;

上面示例表示变量foo是一个指针,指向的还是一个指针,第二个指针指向的则是一个整数。

* 运算符

*这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。

void increment(int* p) {
  *p = *p + 1;
}

上面示例中,函数increment()的参数是一个整数指针p。函数体里面,p就表示指针p所指向的那个值。对p赋值,就表示改变指针所指向的那个地址里面的值。

上面函数的作用是将参数值加1。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。 事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。

变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。

& 运算符

&运算符用来取出一个变量所在的内存地址。

int x = 1;
printf("x's address is %p\n", &x);

上面示例中,x是一个整数变量,&x就是x的值所在的内存地址。printf()的%p是内存地址的占位符,可以打印出内存地址。

上一小节中,参数变量加1的函数,可以像下面这样使用。

void increment(int* p) {
  *p = *p + 1;
}

int x = 1;
increment(&x);
printf("%d\n", x); // 2

上面示例中,调用increment()函数以后,变量x的值就增加了1,原因就在于传入函数的是变量x的地址&x。

&运算符与*运算符互为逆运算,下面的表达式总是成立。

int i = 5;

if (i == *(&i)) // 正确

函数

简介

和js中的函数相比较,C语言中的函数存在以下几点不同

  • 不需要function关键字声明
  • 函数调用时,参数个数必须与定义里面的参数个数一致
  • 函数必须先声明后调用(不存在hoist)
  • 实参和形参的类型不一致时,会自动转换为形参类型
  • 如果没有写返回值类型,默认是int
int plus_one(int n) {
  return n + 1;
}

上面的代码声明了一个函数plus_one()。

函数调用时,参数个数必须与定义里面的参数个数一致,参数过多或过少都会报错。

int plus_one(int n) {
  return n + 1;
}

plus_one(2, 2); // 报错
plus_one();  // 报错

上面示例中,函数plus_one()只能接受一个参数,传入两个参数或不传参数,都会报错。

int a = plus_one(13);

int plus_one(int n) {
  return n + 1;
}

上面示例中,在调用plus_one()之后,才声明这个函数,编译就会报错。

#include <stdio.h>
int main(void)
{
    void change2(double num1, double num2)
    {
        printf("num1 = %.1f\n", num1);
        printf("num2 = %f", num2);
    }
    change2(1.2, 3);
}

上面示例中,自动将实参转为double后保存

main方法

C 语言规定,main()是程序的入口函数,即所有的程序一定要包含一个main()函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。其他函数都是通过它引入程序的。

main()的写法与其他函数一样,要给出返回值的类型和参数的类型,就像下面这样。

int main(void) {
  printf("Hello World\n");
  return 0;
}

上面示例中,最后的return 0;表示函数结束运行,返回0。

C 语言约定,返回值0表示函数运行成功,如果返回其他非零整数,就表示运行失败,代码出了问题。系统根据main()的返回值,作为整个程序的返回值,确定程序是否运行成功。

正常情况下,如果main()里面省略return 0这一行,编译器会自动加上,即main()的默认返回值为0。所以,写成下面这样,效果完全一样。

int main(void) {
  printf("Hello World\n");
}

由于 C 语言只会对main()函数默认添加返回值,对其他函数不会这样做,所以建议总是保留return语句,以便形成统一的代码风格。

系统在程序启动调用main函数会传入以下两个参数

  • 参数个数
  • 每个参数组成的数组,第一个参数总是main函数执行文件的路径,剩余参数皆为执行时传入的参数
#include <stdio.h>
int main(int argc, const char *argv[])
{
    printf("argc = %d\n", argc);
    printf("argv [0] pwd= %s\n", argv[0]);
    printf("argv [1] args%s\n", argv[1]);
};

函数原型

前面说过,函数必须先声明,后使用。由于程序总是先运行main()函数,导致所有其他函数都必须在main()函数之前声明。

但是,main()是整个程序的入口,也是主要逻辑,放在最前面比较好。另一方面,对于函数较多的程序,保证每个函数的顺序正确,会变得很麻烦。

C 语言提供的解决方法是,只要在程序开头处给出函数原型,函数就可以先使用、后声明。所谓函数原型,就是提前告诉编译器,每个函数的返回类型和参数类型。其他信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。

int twice(int);

int main(int num) {
  return twice(num);
}

int twice(int num) {
  return 2 * num;
}

上面示例中,函数twice()的实现是放在main()后面,但是代码头部先给出了函数原型,所以可以正确编译。只要提前给出函数原型,函数具体的实现放在哪里,就不重要了。

函数原型包括参数名也可以,虽然这样对于编译器是多余的,但是阅读代码的时候,可能有助于理解函数的意图。

int twice(int);

// 等同于
int twice(int num);

上面示例中,twice函数的参数名num,无论是否出现在原型里面,都是可以的。

注意,函数原型必须以分号结尾。

一般来说,每个源码文件的头部,都会给出当前脚本使用的所有函数的原型。

exit

exit()函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。

exit()可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:EXIT_SUCCESS(相当于 0)表示程序运行成功,EXIT_FAILURE(相当于 1)表示程序异常中止。这两个常数也是定义在stdlib.h里面。

// 程序运行成功
// 等同于 exit(0);
exit(EXIT_SUCCESS);

// 程序异常中止
// 等同于 exit(1);
exit(EXIT_FAILURE);

在main()函数里面,exit()等价于使用return语句。其他函数使用exit(),就是终止整个程序的运行,没有其他作用。

C 语言还提供了一个atexit()函数,用来登记exit()执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件stdlib.h。

int atexit(void (*func)(void));

atexit()的参数是一个函数指针。注意,它的参数函数(下例的print)不能接受参数,也不能有返回值。

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    void print(void) { printf("somethig run while exit!"); };
    atexit(print);
    exit(EXIT_FAILURE);
}

上面示例中,exit()执行时会先自动调用atexit()注册的print()函数,然后再终止程序。

函数说明符

extern 说明符

对于多文件的项目,源码文件会用到其他文件声明的函数。这时,当前文件里面,需要给出外部函数的原型,并用extern说明该函数的定义来自其他文件。

extern int foo(int arg1, char arg2);

int main(void) {
  int a = foo(2, 3);
  // ...
  return 0;
}

上面示例中,函数foo()定义在其他文件,extern告诉编译器当前文件不包含该函数的定义。

不过,由于函数原型默认就是extern,所以这里不加extern,效果是一样的。

static 说明符

默认情况下,每次调用函数时,函数的内部变量都会重新初始化,不会保留上一次运行的值。static说明符可以改变这种行为。

static用于函数内部声明变量时,表示该变量只需要初始化一次,不需要在每次调用时都进行初始化。也就是说,它的值在两次调用之间保持不变。

#include <stdio.h>

void counter(void) {
  static int count = 1;  // 只初始化一次
  printf("%d\n", count);
  count++;
}

int main(void) {
  counter();  // 1
  counter();  // 2
  counter();  // 3
  counter();  // 4
}

上面示例中,函数counter()的内部变量count,使用static说明符修饰,表明这个变量只初始化一次,以后每次调用时都会使用上一次的值,造成递增的效果。

注意,static修饰的变量初始化时,只能赋值为常量,不能赋值为变量。

int i = 3;
static int j = i; // 错误

上面示例中,j属于静态变量,初始化时不能赋值为另一个变量i。

另外,在块作用域中,static声明的变量有默认值0。

static int foo;
// 等同于
static int foo = 0;

static可以用来修饰函数本身。

static int Twice(int num) {
  int result = num * 2;
  return(result);
}

上面示例中,static关键字表示该函数只能在当前文件里使用,如果没有这个关键字,其他文件也可以使用这个函数(通过声明函数原型)。

static也可以用在参数里面,修饰参数数组。

int sum_array(int a[static 3], int n) {
  // ...
}

上面示例中,static对程序行为不会有任何影响,只是用来告诉编译器,该数组长度至少为3,某些情况下可以加快程序运行速度。 另外,需要注意的是,对于多维数组的参数,static仅可用于第一维的说明。

const 说明符

函数参数里面的const说明符,表示函数内部不得修改该参数变量。

void f(int* p) {
  // ...
}

上面示例中,函数f()的参数是一个指针p,函数内部可能会改掉它所指向的值*p,从而影响到函数外部。

为了避免这种情况,可以在声明函数时,在指针参数前面加上const说明符,告诉编译器,函数内部不能修改该参数所指向的值。

void f(const int* p) {
  *p = 0; // 该行报错
}

上面示例中,声明函数时,const指定不能修改指针p指向的值,所以*p = 0就会报错。

但是上面这种写法,只限制修改p所指向的值,而p本身的地址是可以修改的。

void f(const int* p) {
  int x = 13;
  p = &x; // 允许修改
}

上面示例中,p本身是可以修改,const只限定*p不能修改。

如果想限制修改p,可以把const放在p前面。

void f(int* const p) {
  int x = 13;
  p = &x; // 该行报错
}

如果想同时限制修改p和*p,需要使用两个const。

void f(const int* const p) {
  // ...
}

字符串

特征

C语言的字符串是以字符数组的形态存在的

C语言的字符串具有以下特征:

  • 以0(整数0)结尾的一串字符
  • *0 或 ‘\0’ 是一样的,但是和’0’不同
  • 0 标志字符串的结束,但他不是字符串的一部分
  • 计算字符串长度的时候不包含这个字符
#include <stdio.h>
int main(void)
{
    char *str = "hello"; // 字符指针
    char str2[] = {'a', 'b', '\0'};
    char word[] = "hello";
    char line[10] = "hello11";
}

注意: []中的长度是可以省略不写的; 采用第2种方式的时候最后一个元素必须是’0’,’0’表示字符串的结束标志; 采用第2种方式的时候在数组中不能写中文。 在输出字符串的时候要使用:printf(“%s”,字符数组名字);或者puts(字符数组名字);

字符串常量

#include <stdio.h>
int main(void)
{
    char *str = "Hello World";
    char *str1 = "Hello World1";
    str[0] = 'S'; // 报错
    printf("str  pointer %p\n", str);
    printf("str = %s\n", str);
    printf("str1 pointer %p\n", str1);
}  

其中

char *str = "Hello World";
  • s是一个指针,初始化为指向一个字符串常量。
  • 由于这个是常量所在的地方,所以实际上s是const char* s ,但由于历史原因,编译器接收不带const的写法
  • 试图对s所指的字符串做写入会导致预期之外的后果。
  • 且因为指向的这个字符串是只读的,所以声明相同的字符串常量是指向同一内存空间

所以尽量使用字符串数组

字符串的输入和输出

scanf输入字符串

#include <stdio.h>
int main(void)
{
    char string[8];
    char string1[8];
    scanf("%s", string);
    scanf("%7s", string1);
    printf("%s##%s##\n", string, string1);
}

scanf读入一个单词(到空格、tab或回车为止)

putchar

int putchar(int c);

  • 向标准输出写出一个字符
  • 返回写了几个字符,EOF(-1)表示写失败

getchar

int getchar(void)

  • 从标准输入得到一个字符
  • 返回类型是int,是为了返回EOF(-1)

字符串的方法

strlen()

strlen()函数返回字符串的字节长度,不包括末尾的空字符\0。该函数的原型如下。

#include <string.h>
char* str = "hello";
int len = strlen(str); // 5

strcmp()

strcmp()函数用于比较两个字符串的内容。该函数的原型如下,定义在string.h头文件里面。

int strcmp(const char* s1, const char* s2);

按照字典顺序,如果两个字符串相同,返回值为0;如果s1小于s2,strcmp()返回值小于0;如果s1大于s2,返回值大于0。

下面是一个用法示例。

// s1 = Happy New Year
// s2 = Happy New Year
// s3 = Happy Holidays

strcmp(s1, s2) // 0
strcmp(s1, s3) // 大于 0
strcmp(s3, s1) // 小于 0

注意,strcmp()只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(==)就能比较。所以,不要把字符类型(char)的值,放入strcmp()当作参数。

strcpy()

将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h头文件里面。

strcpy(char dest[], const char source[])

strchr()

char *strchr(const char *s,int c);
char *strrchr(const char *s,int c);
// 返回NULL表示没有找到
char s[] = "hello";
char *p = strchr(s, 'l'); //llo
printf("%s", p);

返回满足该条件开始到结束的字符串

内存管理

进程空间

  • 程序,是经过源码编译后的可执行文件,可执行文件可以多次被执行,比如我可以多次打开谷歌浏览器
  • 进程,是程序加载到内存后开始执行到执行结束,这样一段时间概念,多次打开谷歌浏览器,每打开一次都是一个进程,每当关闭一个浏览器,则表示该进程结束
  • 程序是静态概念,而进程是动态/时间的概念

进程空间示意图 有个进程和空间的概念,程序被加载到内存后内存的空间布局是怎么样的:

Flower and water

struct结构

简介

C 语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用中并不够用。

实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。

  • 复杂的物体需要使用多个变量描述,这些变量都是相关的,最好有某种机制将它们联系起来。
  • 某些函数需要传入多个参数,如果一个个按照顺序传入,非常麻烦,最好能组合成一个复合结构传入。

为了解决这些问题,C 语言提供了struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起。 这样不仅为编程提供方便,也有利于增强代码的可读性。 C 语言没有其他语言的对象(object)和类(class)的概念,struct 结构很大程度上提供了对象和类的功能。

下面是struct自定义数据类型的一个例子。

struct fraction {
  int numerator;
  int denominator;
};
struct fraction f1;
f1.numerator = 22;
f1.denominator = 7;

除了逐一对属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值。

struct car {
  char* name;
  float price;
  int speed;
};

struct car saturn = {"Saturn SL/2", 16000.99, 175};

上面示例中,变量saturn是struct car类型,大括号里面同时对它的三个属性赋值。如果大括号里面的值的数量,少于属性的数量,那么缺失的属性自动初始化为0。

注意,大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。否则,必须为每个值指定属性名。

struct car saturn = {.speed=172, .name="Saturn SL/2"};

上面示例中,初始化的属性少于声明时的属性,这时剩下的那些属性都会初始化为0。

struct 的复制

struct 变量可以使用赋值运算符(=),复制给另一个变量,这时会生成一个全新的副本。 系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据。 这一点跟数组的复制不一样,务必小心。

struct cat { char name[30]; short age; } a, b;

strcpy(a.name, "Hula");
a.age = 3;

b = a;
b.name[0] = 'M';

printf("%s\n", a.name); // Hula
printf("%s\n", b.name); // Mula

struct 指针

#include <stdio.h>

struct turtle {
  char* name;
  char* species;
  int age;
};

void happy(struct turtle t) {
  t.age = t.age + 1;
}

int main() {
  struct turtle myTurtle = {"MyTurtle", "sea turtle", 99};
  happy(myTurtle);
  printf("Age is %i\n", myTurtle.age); // 输出 99
  return 0;
}

上面示例中,函数happy()传入的是一个 struct 变量myTurtle,函数内部有一个自增操作。但是,执行完happy()以后,函数外部的age属性值根本没变。 原因就是函数内部得到的是 struct 变量的副本,改变副本影响不到函数外部的原始数据。

通常情况下,开发者希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。 这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性,就可以影响到函数外部。

struct 指针传入函数的写法如下:

void happy(struct turtle* t) {
}

happy(&myTurtle);

上面代码中,t是 struct 结构的指针,调用函数时传入的是指针。struct 类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成&myTurtle。

函数内部也必须使用(*t).age的写法,从指针拿到 struct 结构本身。

void happy(struct turtle* t) {
  (*t).age = (*t).age + 1;
}

上面示例中,(t).age不能写成t.age,因为点运算符.的优先级高于*。 *t.age这种写法会将t.age看成一个指针,然后取它对应的值,会出现无法预料的结果。

(*t).age这样的写法很麻烦。C 语言就引入了一个新的箭头运算符(->), 可以从 struct 指针上直接获取属性,大大增强了代码的可读性

void happy(struct turtle* t) {
  t->age = t->age + 1;
}
// ptr == &myStruct
myStruct.prop == (*ptr).prop == ptr->prop

自定义数据类型 typedef

简介

typedef命令用来为某个类型起别名。

typedef type name;

上面代码中,type代表类型名,name代表别名。

typedef unsigned char BYTE;

BYTE c = 'z';

联合 union

  1. 在联合类型中,所有成员都是共用同一块内存空间
  2. 同一时间只有一个成员是有效的
  3. union的大小是其最大的成员
#include <stdio.h>

int main(void)
{
    typedef union
    {
        int age;
        char capital;
    } Person;
    Person p;
    p.age = 1;
    p.capital = 'a';
    printf("%c", p.age); // 'a'
}

全局变量和静态本地变量

全局变量的特点

  • 定义在main函数外面的变量
  • 全局变量具有全局的生存期和作用域
  • 他们与任何函数都无关,在任何函数内都可以使用它。
  • 没有做初始化的变量会得到0。(本地变量未初始化则是一串随机的内存空间)
  • 没有做初始化的指针会得到NULL。
int all;
int main(void){
  printf("%d",all); // 0
}

静态本地变量和全局变量的比较

  • 静态本地变量其实是特殊的全局变量
  • 他们位于相同的内存区域
  • 静态本地变量具有全局的生存期,函数内部的作用域
  • static 在这里的意思是局部作用域(本地可访问)

编译预处理指令

编译之前会进行处理的指令

  • #开头的是编译预处理指令
  • 它们不是C语言的成分,但C语言离不开它
  • #define用来定义一个宏

宏的定义

现在我们会如何表示一个不可变的值呢?

const double PI = 3.1415926;

这是在C99之后才支持的const的写法,在此之前常用#define定义一个宏

#define

  1. #define <名字><值>
  2. 注意没有结尾的分号,因为不是C语言的语句
  3. 名称必须是一个单词,值可以是各种东西
  4. 在C语言的编译器在开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值。
  5. 完全本的文本替换
  6. gcc –save-temps可以保存预编译的产物
  7. 如果一个宏中含有其他宏的名字,也是会被替换的
  8. 如果一个宏的值超过一行,最后一行之前的行末需要加\
  9. 定义宏中的注释不会被当作宏的一部分解析
#include <stdio.h>
#define PI 3.13159
#define PI2 2 * PI // PI*2
#define FORMAT "%f\n"
#define print              \
    printf("PI=%f\n", PI); \
    printf("PI2=%f\n", PI2);

int main(void)
{
    printf(FORMAT, PI);
    printf(FORMAT, PI2);
    print;
}

没有值的宏

  • #define _DEBUG
  • 这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了。

预定义的宏

__LINE__  当前行号
__FILE__  文件名
__DATE__  当前日期
__TIME__  当前时间

带参数的宏

定义的原则:

  1. 一切都要括号
  2. 整个值都要括号
  3. 参数出现的每个地方都要括号

#define RADTODEG(x)((x)*57.2233)