理解指针的关键

C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。对,从名字开始,不是从开头也不是从末尾。

知道了这个关键,再知道一些运算符的优先级,它们的优先级从高到低依次是::

  • 定义中被括号( )括起来的那部分。
  • 后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
  • 前缀操作符:星号*表示“指向xxx的指针”。

举例子来说明:

#include <stdio.h>

int main(int argc, const char * argv[]){
    int *p1[6];
    /*
     从 p1 开始理解,它的左边是 *,右边是 [ ],[ ] 的优先级高于 *,所以编译器先解析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int *,它用来说明数组元素的类型。从整体上讲,p1 是一个拥有 6 个 int * 元素的数组,也即指针数组。
     **/
    int (*p3)[6];
    /*
     从 p3 开始理解,( ) 的优先级最高,编译器先解析(*p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是一个拥有 6 个元素的一维数组。从整体上讲,p3 是一个指向拥有 6 个 int 元素数组的指针,也即二维数组指针。
     这里说明一下:为了能够通过指针来遍历数组元素,在定义数组指针时需要进行降维处理,例如三维数组指针实际指向的数据类型是二维数组,二维数组指针实际指向的数据类型是一维数组,一维数组指针实际指向的是一个基本类型;在表达式中,数组名也会进行同样的转换(下降一维)。
     **/
    int (*p4)(int, int);
    /*
     从 p4 开始理解,( ) 的优先级最高,编译器先解析(*p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针。
     **/
    char *(* c[10])(int **p);
    /*
     这个定义有两个名字,分别是 c 和 p,乍看起来 p 是指针变量的名字,不过很遗憾这是错误的。如果 p 是指针变量名,c[10]这种写法就又定义了一个新的名字,这让人匪夷所思。
     以 c 作为变量的名字,先来看括号内部(* c[10])
     [ ] 的优先级高于 *,编译器先解析c[10],c 首先是一个数组,它前面的*表明每个数组元素都是一个指针,只是还不知道指向什么类型的数据。整体上来看,(* c[10])说明 c 是一个指针数组,只是指针指向的数据类型尚未确定。

     跳出括号,根据优先级规则(() 的优先级高于 *)应该先看右边(int **p)
     ( )说明是一个函数,int **p是函数参数。
     再看左边char * 是函数的返回值类型。
     从整体上看,我们可以将定义分成两部分:
     (* c[10])表明 c 是一个指针数组,char *(int **p);表明指针指向的数据类型,合起来就是:c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。
     **/
    int (*(*(*pfunc)(int *))[5])(int *);
    /*
     从 pfunc 开始理解,先看括号内部(*pfunc),pfunc 是一个指针。
     跳出括号,看它的两边,*(int *),根据优先级规则应该先看右边的(int *),它表明这是一个函数,int *是参数列表。再看左边的*,它表明函数的返回值是一个指针,只是指针指向的数据类型尚未确定。
     将上面的两部分合成一个整体,(*(*pfunc)(int *)),它表明 pfunc 是一个指向函数的指针,现在函数的参数列表确定了,也知道返回值是一个指针了(只是不知道它指向什么类型的数据)。
     再向外跳一层括号,[5],[ ] 的优先级高于 *,先看右边,[5] 表示这是一个数组,再看左边,* 表示数组的每个元素都是指针。也就是说,* [5] 是一个指针数组,函数返回的指针就指向这样一个数组。
     那么,指针数组中的指针又指向什么类型的数据呢?再向外跳一层括号,int  (int *);右边,它是一个函数,再看左边,它是函数的返回值类型。也就是说,指针数组中的指针指向原型为int func(int *);的函数。
     将上面的三部分合起来就是:pfunc 是一个函数指针(蓝色部分),该函数的返回值是一个指针,它指向一个指针数组(红色部分),指针数组中的指针指向原型为int func(int *);的函数(橘黄色部分)。
     
     **/
    return 0;
}

常见指针变量的定义

定 义含 义
int *p;p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组。
int **p;p 为二级指针,指向 int * 类型的数据。
int *p[n];p 为指针数组。[ ] 的优先级高于 ,所以应该理解为 int (p[n]);
int (*p)[n];p 为二维数组指针。
int *p();p 是一个函数,它的返回值类型为 int *。
int (*p)();p 是一个函数指针,指向原型为 int func() 的函数。

#include <stdio.h>
int main(int argc, const char * argv[]){
    char *string0 = "COSC1283/1284";
    char *string1 = "Programming";
    char *string2 = "Techniques";
    char *string3 = "is";
    char *string4 = "great fun";
   
    char *lines[5];//定义了一个指针数组,它的每个元素的类型都是char *
    /*
     在表达式中使用 lines 时,它会转换为一个类型为char **的指针,这样*lines就表示一个指向字符的指针,而**lines表示一个具体的字符,这一点很重要!
     **/
    lines[0] = string0;
    lines[1] = string1;
    lines[2] = string2;
    lines[3] = string3;
    lines[4] = string4;
    /*
     指针是可以进行运算的,lines 表示数组的首地址(第0个元素的地址),lines+0、lines+1、lines+2 ... 分别表示第0、1、2 ...个元素的地址,*(lines+0)或lines[0]、*(lines+1)或lines[1]、*(lines+2)或lines[2] ... 分别是字符串 string0, string1, string2 ... 的首地址。即:
     *lines == *(lines+0) == lines[0] == string0
     *(lines+1) == lines[1] == string1
     *(lines+2) == lines[2] == string2
     注意:lines 是二级指针,*(lines+i) 是一级指针,**(lines+i) 才是具体的字符。
     **/
    char *str1 = lines[1];//lines[1]:它是一个指针,指向字符串string1,即 string1 的首地址。
    char *str2 = *(lines + 3);//*(lines + 3):lines + 3 为数组中第 3 个元素的地址,*(lines + 3) 为第 3 个元素的值,它是一个指针,指向字符串 string3。
    char c1 = *(*(lines + 4) + 6);//*(*(lines + 4) + 6):*(lines + 4) + 6 == lines[4] + 6 == string4 + 6,表示字符串 string4 中第 6 个字符的地址,即 f 的地址,所以 *(*(lines + 4) + 6) 就表示字符 f。
    char c2 = (*lines + 5)[5];//(*lines + 5)[5]:*lines + 5 为字符串 string0 中第 5 个字符的地址,即 2 的地址,(*lines + 5)[5]等价于*(*lines + 5 + 5),表示第10个字符,即 2。
    char c3 = *lines[0] + 2;//*lines[0] + 2:lines[0] 为字符串 string0 中第 0 个字符的地址,即 C 的地址;*lines[0] 也就表示第 0 个字符,即字符 C。字符与整数运算,首先转换为该字符对应的 ASCII 码,然后再运算,所以 *lines[0] + 2 = 67 + 2 = 69,69 对应的字符为 E。
    printf("str1 = %s\n", str1);
    printf("str2 = %s\n", str2);
    printf("  c1 = %c\n", c1);
    printf("  c2 = %c\n", c2);
    printf("  c3 = %c\n", c3);
    return 0;
}

#include <stdio.h>

int main(int argc, const char * argv[]) {
    
    char str[20] = "c.biancheng.net";

    char *s1 = str;
    char *s2 = str+2;

    char c1 = str[4];
    char c2 = *str;
    char c3 = *(str+4);
    char c4 = *str+2;
    char c5 = (str+1)[5];

    int num1 = *str+2;
    long num2 = (long)str;
    long num3 = (long)(str+2);
    
    printf("  s1 = %s\n", s1);//str 既是数组名称,也是一个指向字符串的指针;指针可以参加运算,加 1 相当于数组下标加 1。
    printf("  s2 = %s\n", s2);
    //printf() 输出字符串时,要求给出一个起始地址,并从这个地址开始输出,直到遇见字符串结束标志\0。s1 为字符串 str 第 0 个字符的地址,s2 为第 2 个字符的地址,所以 printf() 的结果分别为 c.biancheng.net 和 biancheng.net。
    printf("  c1 = %c\n", c1);//从0开始数,第四个位置,字符是a
    printf("  c2 = %c\n", c2);//输出一个字符,首个字符是c
    printf("  c3 = %c\n", c3);//指针可以参加运算,str+4 表示第 4 个字符的地址,c3 = *(str+4) 表示第4个字符,即 'a'。
    
    printf("  c4 = %c\n", c4);//字符与整数运算时,先转换为整数(字符对应的ASCII码)。num1 与 c4 右边的表达式相同,对于 num1,*str+2 == 'c'+2 == 99+2 == 101,即 num1 的值为 101,对于 c4,101 对应的字符为 ‘e’,所以 c4 的输出值为 'e'。
    printf("  c5 = %c\n", c5);
    //其实,数组元素的访问形式可以看做 address[offset],address 为起始地址,offset 为偏移量:c1 = str[4]表示以地址 str 为起点,向后偏移4个字符,为 'a';c5 = (str+1)[5]表示以地址 str+1 为起点,向后偏移5个字符,等价于str[6],为 'c'。
    printf("num1 = %d\n", num1);//看C4说明
    //num2 和 num3 分别为字符串 str 的首地址和第 2 个元素的地址。
    printf("num2 = %ld\n", num2);
    printf("num3 = %ld\n", num3);
    return 0;
}

求长方体的长宽高求它的体积以及三个面的面积。

分析:体积V=lengthwidthheight,三个面的面积分别为s1=lengthwidth、s2=widthheight、s3=length*height
C语言中的函数只能有一个返回值,我们只能将其中的一份数据,也就是体积 v 放到返回值中,而将面积 s1、s2、s3 设置为全局变量。

#include <stdio.h>

int s1,s2,s3;//定义三个全局变量用来接收面积
int vs(int a,int b,int c);//声明一下函数

int main(int argc, const char * argv[]) {
    int v,length,width,height;
    printf("Input length, width and height: ");
    scanf("%d,%d,%d",&length,&width,&height);
    v = vs(length,width,height);
    printf("v=%d,s1=%d,s2=%d,s3=%d\n",v,s1,s2,s3);
    return 0;
}

int vs(int a,int b,int c){
    int v;
    v = a*b*c;//体积
    s1 = a*b;
    s2 = a*c;
    s3 = b*c;
    return v;
}

求两个整数的最大公约数。

分析:最大公因数,也称最大公约数、最大公因子,指两个或多个整数共有约数中最大的一个。例如:求24和60的最大公约数,先分解质因数,得24=2×2×2×3,60=2×2×3×5,24与60的全部公有的质因数是2、2、3,它们的积是2×2×3=12,所以,(24,60)=12。

#include <stdio.h>

int gcd(int a,int b);

int main(int argc,const char * argv[]){
    int a,b;
    scanf("%d,%d",&a,&b);
    printf("GCD: A=>%d, B=>%d (A,B)=%d\n",a,b,gcd(a,b));
    return 0;
}
int gcd(int a,int b){
    /*
     * 最大公约数的递归:
     * 1、若a可以整除b,则最大公约数是b
     * 2、如果1不成立,最大公约数便是b与a%b的最大公约数
     * 示例:求(140,21)
     * 140%21 = 14
     * 21%14 = 7
     * 14%7 = 0
     * 返回7
     * 示例:求(21,140)
     * 21%140 = 21
     * 140%21 = 14
     * ...
     * 返回7
     * */
    if(a%b==0){
        return b;
    }else{
        return gcd(b, a%b);
    }
}

查看给定的字符是否位于某个字符串中。

分析:遍历给的字符串中有字符和输入的字符相等

#include <stdio.h>
#include <string.h>

int strchar(char *str, char c);
int main(){
    char url[] = "zhangkai";
    char letter = 'a';
    if(strchar(url, letter) >= 0){
        printf("The letter is in the string.\n");
    }else{
        printf("The letter is not in the string.\n");
    }
    return 0;
}
int strchar(char *str, char c){
    for(int i=0,len = strlen(str); i<len; i++){  //i和len都是块级变量
        if(str[i] == c){
            return i;
        }
    }
    return -1;
}

字符串反转(逆置)

分析:

  • 非递归:遍历字符串,交换前后两个相应位置的字符;
  • 递归:将第一个字符保存在tmp中,将最后一个字符赋给第一个字符,递归调用。每次调用函数,都会把字符串的第 0 个字符保存到 ctemp 变量,并把最后一个字符填充到第 0 个字符的位置,同时用'0'来填充最后一个字符的位置。
#include <stdio.h>
#include <string.h>
char *reverse(char *str);
int main(int argc,const char * argv[]){
    char str[]="abcdefg";
    printf("%s\n",reverse(str));
    return 0;
}
char *reverse(char *str) {
    int len = strlen(str);//计算字符串长度,用来查找最后一个字符
    if (len > 1) {
        char ctemp = str[0];//将第一个字符保存临时变量里
        str[0] = str[len - 1];//将最后一个字符赋值给第一个字符
        str[len - 1] = '\0'; //交换后指针指向下一个字符,最后一个字符赋为’\0’
        reverse(str + 1);  //递归调用
        str[len - 1] = ctemp;//将保存的tmp值赋给左后一个字符
    }
    return str;
}

求菲波那契数

分析:菲波那契数就是一个数列,数列中每个数的值就是它前面两个数的和,这种关系常常用以下形式进行描述:
F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*)。

#include <stdio.h>
//递归计算斐波那契数
//双层递归的调用关系和数据结构中二叉树的结构完全吻合,所以双层递归常用于二叉树的遍历。
long fib(int n) {
    if (n <= 2) {
        return 1;
    }
    else {
        return fib(n - 1) + fib(n - 2);
    }
}
int main() {
    int a;
    printf("Input a number: ");
    scanf("%d", &a);
    printf("Fib(%d) = %ld\n", a, fib(a));
    return 0;
}

C语言源文件要经过编译、链接才能生成可执行程序:

  1. 编译(Compile)会将源文件(.c文件)转换为目标文件。对于 VC/VS,目标文件后缀为.obj;对于GCC,目标文件后缀为.o。
  2. 链接(Link)是针对多个文件的,它会将编译生成的多个目标文件以及系统中的库、组件等合并成一个可执行程序。

预处理主要是处理以#开头的命令

#include <stdio.h>

//不同的平台下引入不同的头文件
#if _WIN32 //Widows平台
#include <windows.h>
#elif __linux__  //识别linux平台
#include <unistd.h>
#endif

int main(int argc, const char * argv[]) {
    //不同的平台下调用不同的函数
    #if _WIN32 //widows
    Sleep(5000); //Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds)(注意 S 是大写的),参数的单位是“毫秒”
    #elif __linux__ //linux
    sleep(5); //Linux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds),参数的单位是“秒”,
    #endif
    return 0;
}

include用法

#include 叫做文件包含命令,用来引入对应的头文件(.h文件)。用法有两种:
1.#include <stdHeader.h>
2.#include "myHeader.h"

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:

  • 使用尖括号< >,编译器会到系统路径下查找头文件;
  • 而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
  • 一般习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。

define

宏定义命令,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。

#define  宏名  字符串
  • 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
  • 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
  • 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
  • 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替
  • 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
  • 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
  • 可用宏定义表示数据类型,使书写方便

带参数的宏

#define 宏名(形参列表) 字符串
例:
#define M(y) y*y+3*y  //宏定义
k=M(5);  //宏调用
//在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=5*5+3*5。
  • 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
  • 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
  • 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
    注:带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。

##的用法

##称为连接符,用来将宏参数或其他的串连接起来。

#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
//展开为
printf("%f\n", 8.5e2);
printf("%d\n", 123400);

常用宏

ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用:

  • __LINE__:表示当前源代码的行号;
  • __FILE__:表示当前源文件的名称;
  • __DATE__:表示当前的编译日期;
  • __TIME__:表示当前的编译时间;
  • __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
  • __cplusplus:当编写C++程序时该标识符被定义。