【语言学习】C语法与API - hippowc/hippowc.github.io GitHub Wiki

语法

标识符

一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)

数据类型

  • 基本类型:整数类型和浮点类型。char,unsigned char,signed char,int,unsigned int,short,unsigned short,long,unsigned long,float,double,long double
  • 枚举类型:用来定义在程序中只能赋予其一定的离散整数值的变量。
  • void 类型:类型说明符 void 表明没有可用的值。
  • 派生类型:指针类型、数组类型、结构类型、共用体类型和函数类型。

void类型:

  • 函数返回为空:C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
  • 函数参数为空:C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
  • 指针指向 void:类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型

在C或C++语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换;这只是一种简单的代换,字符串可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

  • 无参数宏定义 #define 标识符 字符串
  • 带参数宏定义 #define 宏名(形参表) 字符串
#define M(y) y*y+3*y
k=M(5);

变量定义

变量声明向编译器保证变量以指定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。

int i, j, k; 声明并定义了变量 i、j 和 k,这指示编译器创建类型为 int 的名为 i、j、k 的变量。

extern int d = 3, f = 5;    // d 和 f 的声明与初始化
int d = 3, f = 5;           // 定义并初始化 d 和 f
byte z = 22;                // 定义并初始化 z
char x = 'x';               // 变量 x 的值为 'x'

变量的声明有两种情况:

  • 一种是需要建立存储空间的。例如:int a 在声明的时候就已经建立了存储空间。
  • 另一种是不需要建立存储空间的,通过使用extern关键字声明变量名而不定义它。 例如:extern int a 其中变量 a 可以在别的文件中定义的。

常量

常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。

整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。

85         /* 十进制 */
0213       /* 八进制 */
0x4b       /* 十六进制 */
30         /* 整数 */
30u        /* 无符号整数 */
30l        /* 长整数 */
30ul       /* 无符号长整数 */

字符常量可以是一个普通的字符(例如 'x')、一个转义序列(例如 '\t'),或一个通用的字符(例如 '\u02C0')

字符串字面值或常量是括在双引号 "" 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。

两种简单的定义常量的方式:

  • 使用 #define 预处理器。
  • 使用 const 关键字。
#define identifier value
const type variable = value;

存储类

存储类定义 C 程序中变量/函数的范围(可见性)和生命周期。这些说明符放置在它们所修饰的类型之前。

auto 存储类是所有局部变量默认的存储类。

{
   int mount;
   auto int month;
}

register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 '&' 运算符(因为它没有内存位置)。

{
   register int  miles;
}

static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。全局声明的一个 static 变量或方法可以被任何函数或方法调用,只要这些方法出现在跟 static 变量或方法同一个文件中。

extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。当您使用 'extern' 时,对于无法初始化的变量,会把变量名指向一个之前定义过的存储位置。当您有多个文件且定义了一个可以在其他文件中使用的全局变量或函数时,可以在其他文件中使用 extern 来得到已定义的变量或函数的引用。可以这么理解,extern 是用来在另一个文件中声明一个全局变量或函数。

判断语句

if(boolean_expression)
{
   /* 如果布尔表达式为真将执行的语句 */
} else
{
   /* 如果布尔表达式为假将执行的语句 */
}

? : 运算符(三元运算符)

Exp1 ? Exp2 : Exp3;

switch

switch(表达式)
{
    case 常量表达式1:语句1;
    case 常量表达式2:语句2;
    ...
    default:语句n+1;
}

循环

for(; ;), while, do ... while

for ( init; condition; increment )
{
   statement(s);
}

while(condition)
{
   statement(s);
}

do
{
   statement(s);

}while( condition );

函数

return_type function_name( parameter list )
{
   body of the function
}

作用域规则

局部变量、全局变量和形式参数。

数组

存储一个固定大小的相同类型元素的顺序集合。

type arrayName [ arraySize ];
double balance[10];
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};
balance[4] = 50.0;

枚举

第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

enum 枚举名 {枚举元素1,枚举元素2,……};
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
enum
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

指针

通过指针,可以简化一些 C 编程任务的执行,还有一些任务,如动态内存分配,没有指针是无法执行的。每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址。

#include <stdio.h>
 
int main ()
{
   int  var1;
   char var2[10];
 
   printf("var1 变量的地址: %p\n", &var1  );
   printf("var2 变量的地址: %p\n", &var2  );
 
   return 0;
}

var1 变量的地址: 0x7fff5cc109d4
var2 变量的地址: 0x7fff5cc109de

指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var-name;

int    *ip;    /* 一个整型的指针 */
double *dp;    /* 一个 double 型的指针 */
float  *fp;    /* 一个浮点型的指针 */
char   *ch;     /* 一个字符型的指针 */

所有指针的值的实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

使用指针时会频繁进行以下几个操作:定义一个指针变量、把变量地址赋值给指针、访问指针变量中可用地址的值。这些是通过使用一元运算符 * 来返回位于操作数所指定地址的变量的值。

#include <stdio.h>
 
int main ()
{
   int  var = 20;   /* 实际变量的声明 */
   int  *ip;        /* 指针变量的声明 */
 
   ip = &var;  /* 在指针变量中存储 var 的地址 */
 
   printf("Address of var variable: %p\n", &var  );
 
   /* 在指针变量中存储的地址 */
   printf("Address stored in ip variable: %p\n", ip );
 
   /* 使用指针访问值 */
   printf("Value of *ip variable: %d\n", *ip );
 
   return 0;
}

Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为空指针。NULL 指针是一个定义在标准库中的值为零的常量。按照惯例,如果指针包含空值(零值),则假定它不指向任何东西。

int  *ptr = NULL;
ptr 的地址是 0x0

函数指针

函数指针是指向函数的指针变量。通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。函数指针可以像一般函数一样,用于调用函数、传递参数。

int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型, typedef是返回值类型

int max(int x, int y)
{
    return x > y ? x : y;
}
/* p 是函数指针 */
int (* p)(int, int) = & max; // &可以省略 max 是一个函数
p(a, b) // 与直接调用函数等价 max(a, b)

函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。

void populate_array(int *array, size_t arraySize, int (*getNextValue)(void)) {...}
// 获取随机值
int getNextRandomValue(void)
{
    return rand();
}
populate_array(myarray, 10, getNextRandomValue);

字符串

在 C 语言中,字符串实际上是使用 null 字符 '\0' 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。下面的声明和初始化创建了一个 "Hello" 字符串。由于在数组的末尾存储了空字符,所以字符数组的大小比单词 "Hello" 的字符数多一个。

char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char greeting[] = "Hello";

结构体

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

为了定义结构,您必须使用 struct 语句。struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:

struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;

tag 是结构体标签。member-list 是标准的变量定义,比如 int i; 或者 float f,或者其他有效的变量定义。variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量。下面是声明 Book 结构的方式:

struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book;

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct 
{
    int a;
    char b;
    double c;
} s1;
 
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
    int a;
    char b;
    double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
 
//也可以用typedef创建新类型
typedef struct
{
    int a;
    char b;
    double c; 
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;

结构体变量的初始化, 为了访问结构的成员,我们使用成员访问运算符(.)

#include <stdio.h>
 
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456};
 
int main()
{
    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}

您可以把结构作为函数参数,传参方式与其他类型的变量或指针类似。您可以使用上面实例中的方式来访问结构变量:

/* 函数声明 */
void printBook( struct Books book );

您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似

struct Books *struct_pointer;
struct_pointer = &Book1;

位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态,用 1 位二进位即可。为了节省存储空间,并使处理简便,C 语言又提供了一种数据结构,称为"位域"或"位段"。

所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域定义与结构定义相仿,其形式为:

struct 位域结构名 
{
 位域列表
};

其中位域列表的形式为:

类型说明符 位域名: 位域长度 
struct bs{
    int a:8;
    int b:2;
    int c:6;
} data;
说明 data 为 bs 变量,共占两个字节。其中位域 a 占 8 位,位域 b 占 2 位,位域 c 占 6 位。

位域的使用和结构成员的使用相同,其一般形式为:

位域变量名.位域名
位域变量名->位域名

共用体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。

为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下:

union [union tag]
{
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];

union Data
{
   int i;
   float f;
   char  str[20];
} data;

Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的

为了访问共用体的成员,我们使用成员访问运算符(.)。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
 
   printf( "data.i : %d\n", data.i);
   printf( "data.f : %f\n", data.f);
   printf( "data.str : %s\n", data.str);
 
   return 0;
}

data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming

可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。

typedef

C 语言提供了 typedef 关键字,您可以使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE:

typedef unsigned char BYTE;

typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

预处理器

#define 定义宏 #include 包含一个源代码文件 #undef 取消已定义的宏 #ifdef 如果宏已经定义,则返回真 #ifndef 如果宏没有定义,则返回真 #if 如果给定条件为真,则编译下面代码 #else #if 的替代方案 #elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 #endif 结束一个 #if……#else 条件编译块 #error 当遇到标准错误时,输出错误消息 #pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型

(type_name) expression

错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。C 语言提供了 perror() 和 strerror()

三个必要的头文件

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

一个声明

extern int errno;

四个重要函数

// 检查流中是否有错误,如果有则返回一个非0值,如果没有错误则返回 0
int ferror(FILE* stream);
// 用于检查是否到了文件尾,经常用于文件的读取工作,读取到文件尾则返回非零值
int feof(FILE* stream);
// 当某个函数或者操作触发了设置 errno 变量的时候,我们可以立即使用该函数进行显示输出
char *strerror(int errnum);
strerror(errno);
// 与上方的函数相同,也是用来输出错误的,只不过它自动使用 errno 变量,并且在自前方添加所传入的参数以及一个冒号
void perror(const char *s);
perror("随意的值");

打印函数用法

  • printf是标准输出流的输出函数,用来向屏幕这样的标准输出设备输出
  • fprintf则是向文件输出,将输出的内容输出到硬盘上的文件或是相当于文件的设备上
  • printf是有缓冲的输出,fprintf没有缓冲

使用形式:

fprintf(文件指针,"输出格式",输出项系列);
fprintf(stderr,"错误信息");

// TODO

可变参数

int func(int, ... ) 
{
}
 
int main()
{
   func(2, 2, 3);
   func(3, 2, 3, 4);
}

内存管理

C 语言为内存的分配和管理提供了几个函数。这些函数可以在 <stdlib.h> 头文件中找到。

编程时,如果您预先知道数组的大小,那么定义数组时就比较容易。例如,一个存储人名的数组,它最多容纳 100 个字符,所以您可以定义数组,如下所示:

char name[100];

但是,如果您预先不知道需要存储的文本长度,例如您向存储有关一个主题的详细描述。在这里,我们需要定义一个指针,该指针指向未定义所需内存大小的字符,后续再根据需求来分配内存,如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
   char name[100];
   char *description;
 
   strcpy(name, "Zara Ali");
 
   /* 动态分配内存 */
   description = (char *)malloc( 200 * sizeof(char) );
   if( description == NULL )
   {
      fprintf(stderr, "Error - unable to allocate required memory\n");
   }
   else
   {
      strcpy( description, "Zara ali a DPS student in class 10th");
   }
   printf("Name = %s\n", name );
   printf("Description: %s\n", description );
}

上面的程序也可以使用 calloc() 来编写,只需要把 malloc 替换为 calloc 即可,如下所示

calloc(200, sizeof(char));

当动态分配内存时,您有完全控制权,可以传递任何大小的值。而那些预先定义了大小的数组,一旦定义则无法改变大小。当程序退出时,操作系统会自动释放所有分配给程序的内存,但是,建议您在不需要内存时,都应该调用函数 free() 来释放内存。或者,您可以通过调用函数 realloc() 来增加或减少已分配的内存块的大小。

尝试一下不重新分配额外的内存,strcat() 函数会生成一个错误,因为存储 description 时可用的内存不足。

相关函数

void *calloc(int num, int size);
在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。

void free(void *address); 
该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。

void *malloc(int num); 
在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。

void *realloc(void *address, int newsize); 
该函数重新分配内存,把内存扩展到 newsize。

命令行参数

执行程序时,可以从命令行传值给 C 程序。这些值被称为命令行参数,它们对程序很重要,特别是当您想从外部控制程序,而不是在代码内对这些值进行硬编码时,就显得尤为重要了。命令行参数是使用 main() 函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数。下面是一个简单的实例,检查命令行是否有提供参数,并根据参数执行相应的动作:

#include <stdio.h>

int main( int argc, char *argv[] )  
{
   if( argc == 2 )
   {
      printf("The argument supplied is %s\n", argv[1]);
   }
   else if( argc > 2 )
   {
      printf("Too many arguments supplied.\n");
   }
   else
   {
      printf("One argument expected.\n");
   }
}

指针详解

类型说明,先与什么结合就代表是什么类型

int p; //这是一个普通的整型变量 
 
int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针  

int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组 
 
int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组  

int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针  

int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.  

int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据  

Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针 
 
int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数. 

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。

指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的,以单元为单位。

char a[20];  
int *ptr=(int *)a; //强制类型转换并不会改变a 的类型  
ptr++;

指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整型变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32 位程序中,是被加上了4,因为在32 位程序中,int 占4 个字节。由于地址是用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四个字节。

一个指针ptrold 加(减)一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,ptrnew 所指向的类型和ptrold所指向的类型也相同。ptrnew 的值将比ptrold 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptrold 所指向的类型)个字节。指针和指针进行加减:两个指针不能进行加法运算,这是非法操作,因为进行加法后,得到的结果指向一个不知所向的地方,而且毫无意义。两个指针可以进行减法操作,但必须类型相同,一般用在数组方面

运算符&和*

这里&是取地址运算符,是间接运算符。&a 的运算结果是一个指针,指针的类型是a 的类型加个,指针所指向的类型是a 的类型.*p 的结果是p 所指向的东西,这个东西有这些特点:它的类型是p 指向的类型,它所占用的地址是p所指向的地址。

int a=12; int b; int *p; int **ptr;  
p=&a; //&a 的结果是一个指针,类型是int*,指向的类型是  
//int,指向的地址是a 的地址。  
*p=24; //*p 的结果,在这里它的类型是int,它所占用的地址是  
//p 所指向的地址,显然,*p 就是变量a。  
ptr=&p; //&p 的结果是个指针,该指针的类型是p 的类型加个*,  
//在这里是int **。该指针所指向的类型是p 的类型,这  
//里是int*。该指针所指向的地址就是指针p 自己的地址。  
*ptr=&b; //*ptr 是个指针,&b 的结果也是个指针,且这两个指针  
//的类型和所指向的类型是一样的,所以用&b 来给*ptr 赋  
//值就是毫无问题的了。  
**ptr=34; //*ptr 的结果是ptr 所指向的东西,在这里是一个指针,  
//对这个指针再做一次*运算,结果是一个int 类型的变量。

数组和指针的关系

数组的数组名其实可以看作一个指针。

int array[10]={0,1,2,3,4,5,6,7,8,9},value;  
value=array[0]; //也可写成:value=*array;  
value=array[3]; //也可写成:value=*(array+3);  
value=array[4]; //也可写成:value=*(array+4);

一般而言数组名array 代表数组本身,类型是int[10],但如果把array 看做指针的话,它指向数组的第0 个单元,类型是int* 所指向的类型是数组单元的类型即int。因此array 等于0 就一点也不奇怪了。同理,array+3 是一个指向数组第3 个单元的指针,所以(array+3)等于3。

指针和结构类型的关系

可以声明一个指向结构类型对象的指针。

struct MyStruct  
    {  
        int a;  
        int b;  
        int c;  
    };  
    struct MyStruct ss={20,30,40};  
    //声明了结构对象ss,并把ss 的成员初始化为20,30 和40。  
    struct MyStruct *ptr=&ss;  
    //声明了一个指向结构对象ss 的指针。它的类型是  
    //MyStruct *,它指向的类型是MyStruct。  
    int *pstr=(int*)&ss;  
    //声明了一个指向结构对象ss 的指针。但是pstr 和  
    //它被指向的类型ptr 是不同的。

请问怎样通过指针ptr 来访问ss 的三个成员变量?

ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者 ptr->b; ptr->c;

又请问怎样通过指针pstr 来访问ss 的三个成员变量?

*pstr; //访问了ss 的成员a。 *(pstr+1); //访问了ss 的成员b。 *(pstr+2) //访问了ss 的成员c。

指针和函数的关系

可以把一个指针声明成为一个指向函数的指针。

#include <stdio.h>
#include <stdlib.h>

void (*funP)(int);   //声明也可写成void(*funP)(int x),但习惯上一般不这样。
void (*funA)(int);
void myFun(int x);    //声明也可写成:void myFun( int );
int main()
{
    //一般的函数调用
    myFun(100);

    //myFun与funP的类型关系类似于int 与int *的关系。
    funP=&myFun;  //将myFun函数的地址赋给funP变量
    (*funP)(200);  //通过函数指针变量来调用函数

    //myFun与funA的类型关系类似于int 与int 的关系。
    funA=myFun;
    funA(300);

    //三个貌似错乱的调用
    funP(400);
    (*funA)(600);
    (*myFun)(1000);

    return 0;
}

void myFun(int x)
{
    printf("myFun: %d\n",x);
}
  • myFun的函数名与funP、funA函数指针都是一样的,即都是函数指针。myFun函数名是一个函数指针常量,而funP、funA是函数数指针变量,这是它们的关系
  • 但函数名调用如果都得如(*myFun)(10)这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许myFun(10)这种形式地调用(这样方便多了,并与数学中的函数形式一样)。
  • 为了统一调用方式,funP函数指针变量也可以funP(10)的形式来调用。
  • 赋值时,可以写成funP=&myFun形式,也可以写成funP=myFun。
  • 但是在声明时,void myFun(int )不能写成void (*myFun)(int )。void (*funP)(int )不能写成void funP(int )。
  • 函数指针变量也可以存入一个数组内。数组的声明方法:int (*fArray[10]) ( int );
//前加一个typedef关键字,这样就定义一个名为FunType函数指针类型,而不是一个FunType变量。
typedef void(*FunType)(int);
FunType fp

指针类型转换

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。

float f=12.3;  
float *fptr=&f;  
int *p;

在上面的例子中,假如我们想让指针p 指向实数f,应该怎么办

指针p 的类型是int *,它指向的类型是int。表达式&f 的结果是一个指针,指针的类型是float *,它指向的类型是float。两者不一致,直接赋值的方法是不行的。需要进行"强制类型转换":

p=(int*)&f;

如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP *TYPE, 那么语法格式是: (TYPE *)p;

指针的安全问题

char s='a';  
int *ptr;  
ptr=(int *)&s;  
*ptr=1298;

指针ptr 是一个int *类型的指针,它指向的类型是int。它指向的地址就是s 的首地址。在32 位程序中,s 占一个字节,int 类型占四个字节。最后一条语句不但改变了s 所占的一个字节,还把和s 相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。

API学习

socket

⚠️ **GitHub.com Fallback** ⚠️