C语言的“悔悟”

C language logo

首先讨论指针和数组http://blog.jobbole.com/44863/

The 5-minute Guide to C Pointers

指针、引用和取值

什么是指针?什么是内存地址?什么叫做指针的取值?指针是一个存储计算机内存地址的变量。在这份教程里“引用”表示计算机内存地址。从指针指向的内存读取数据称作指针的取值。指针可以指向某些具体类型的变量地址,例如int、long和double。指针也可以是void类型、NULL指针和未初始化指针。本文会对上述所有指针类型进行探讨。

根据出现的位置不同,操作符 * 既可以用来声明一个指针变量,也可以用作指针的取值。当用在声明一个变量时,*表示这里声明了一个指针。其它情况用到*表示指针的取值。

&是地址操作符,用来引用一个内存地址。通过在变量名字前使用&操作符,我们可以得到该变量的内存地址。

1
2
3
4
5
6
7
8
9
// 声明一个int指针
int *ptr;
// 声明一个int值
int val = 1;
// 为指针分配一个int值的引用
ptr = &val;
// 对指针进行取值,打印存储在指针地址中的内容
int deref = *ptr;
printf("%d\n", deref);

第2行,我们通过*操作符声明了一个int指针。接着我们声明了一个int变量并赋值为1。然后我们用int变量的地址初始化我们的int指针。接下来对int指针取值,用变量的内存地址初始化int指针。最终,我们打印输出变量值,内容为1。

第6行的&val是一个引用。在val变量声明并初始化内存之后,通过在变量名之前使用地址操作符&我们可以直接引用变量的内存地址。

第8行,我们再一次使用*操作符来对该指针取值,可直接获得指针指向的内存地址中的数据。由于指针声明的类型是int,所以取到的值是指针指向的内存地址存储的int值。

这里可以把指针、引用和值的关系类比为信封、邮箱地址和房子。一个指针就好像是一个信封,我们可以在上面填写邮寄地址。一个引用(地址)就像是一个邮件地址,它是实际的地址。取值就像是地址对应的房子。我们可以把信封上的地址擦掉,写上另外一个我们想要的地址,但这个行为对房子没有任何影响。

void指针、NULL指针和未初始化指针 一个指针可以被声明为void类型,比如void *x。一个指针可以被赋值为NULL。一个指针变量声明之后但没有被赋值,叫做未初始化指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int *uninit; // int指针未初始化
int *nullptr = NULL; // 初始化为NULL
void *vptr; // void指针未初始化
int val = 1;
int *iptr;
int *castptr;
 
// void类型可以存储任意类型的指针或引用
iptr = &val;
vptr = iptr;
printf("iptr=%p, vptr=%p\n", iptr, vptr);
 
// 通过显示转换,我们可以把一个void指针转成
// int指针并进行取值
castptr = (int *)vptr;
printf("*castptr=%d\n", *castptr);
 
// 打印null和未初始化指针
printf("uninit=%p, nullptr=%p\n", uninit, nullptr);
// 不知道你会得到怎样的返回值,会是随机的垃圾地址吗?
// printf("*nullptr=%d\n", nullptr);
// 这里会产生一个段错误
// printf("*nullptr=%d\n", nullptr);

执行上面的代码,你会得到类似下面对应不同内存地址的输出。

1
2
3
iptr=0x7fff94b89c6c, vptr=0x7fff94b89c6c
*castptr=1
uninit=0x7fff94b89d50, nullptr=(nil)

第1行我们声明了一个未初始化int指针。所有的指针在赋值为NULL、一个引用(地址)或者另一个指针之前都是未被初始化的。第2行我们声明了一个NULL指针。第3行声明了一个void指针。第4行到第6行声明了一个int值和几个int指针。

第9行到11行,我们为int指针赋值为一个引用并把int指针赋值为void指针。void指针可以保存各种其它指针类型。大多数时候它们被用来存储数据结构。可以注意到,第11行我们打印了int和void指针的地址。它们现在指向了同样的内存地址。所有的指针都存储了内存地址。它们的类型只在取值时起作用。

第15到16行,我们把void指针转换为int指针castptr。请注意这里需要显示转换。虽然C语言并不要求显示地转换,但这样会增加代码的可读性。接着我们对castptr指针取值,值为1。

第19行非常有意思,在这里打印未初始化指针和NULL指针。值得注意的是,未初始化指针是有内存地址的,而且是一个垃圾地址。不知道这个内存地址指向的值是什么。这就是为什么不要对未初始化指针取值的原因。最好的情况是你取到的是垃圾地址接下来你需要对程序进行调试,最坏的情况则会导致程序崩溃。

NULL指针被初始化为o。NULL是一个特殊的地址,用NULL赋值的指针指向的地址为0而不是随机的地址。只有当你准备使用这个地址时有效。不要对NULL地址取值,否则会产生段错误。

 

指针和数组

C语言的数组表示一段连续的内存空间,用来存储多个特定类型的对象。与之相反,指针用来存储单个内存地址。数组和指针不是同一种结构因此不可以互相转换。而数组变量指向了数组的第一个元素的内存地址。

一个数组变量是一个常量。即使指针变量指向同样的地址或者一个不同的数组,也不能把指针赋值给数组变量。也不可以将一个数组变量赋值给另一个数组。然而,可以把一个数组变量赋值给指针,这一点似乎让人感到费解。把数组变量赋值给指针时,实际上是把指向数组第一个元素的地址赋给指针。

1
2
3
4
5
6
7
8
int myarray[4] = {1,2,3,0};
int *ptr = myarray;
printf("*ptr=%d\n", *ptr);
 
// 数组变量是常量,不能做下面的赋值
// myarray = ptr
// myarray = myarray2
// myarray = &myarray2[0]

第1行初始化了一个int数组,第2行用数组变量初始化了一个int指针。由于数组变量实际上是第一个元素的地址,因此我们可以把这个地址赋值给指针。这个赋值与int *ptr = &myarray[0]效果相同,显示地把数组的第一个元素地址赋值到了ptr引用。这里需要注意的是,这里指针需要和数组的元素类型保持一致,除非指针类型为void。

 

指针与结构体

就像数组一样,指向结构体的指针存储了结构体第一个元素的内存地址。与数组指针一样,结构体的指针必须声明和结构体类型保持一致,或者声明为void类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct person {
  int age;
  char *name;
};
struct person first;
struct person *ptr;
 
first.age = 21;
char *fullname = "full name";
first.name = fullname;
ptr = &first;
 
printf("age=%d, name=%s\n", first.age, ptr->name);

第1至6行声明了一个person结构体,一个变量指向了一个person结构体和指向person结构体的指针。第8行为age成员赋了一个int值。第9至10行我们声明了一个char指针并赋值给一个char数组并赋值给结构体name成员。第11行我们把一个person结构体引用赋值给结构体变量。

第13行我们打印了结构体实例的age和name。这里需要注意两个不同的符号,’.’ 和 ‘->’ 。结构体实例可以通过使用 ‘.’ 符号访问age变量。对于结构体实例的指针,我们可以通过 ‘->’ 符号访问name变量。也可以同样通过(*ptr).name来访问name变量。

原文链接: Dennis Kubes   翻译: 伯乐在线 - 唐尤华
译文链接: http://blog.jobbole.com/25409/

C does not directly support pass by reference because it always uses pass by value, but a programmer can implement pass by reference by passing a pointer to the variable that the programmer wants passed by reference

在C语言中并没有按引用传递的用法(C++中利用&),但是可以利用指针间接实现同引用传递相同的目的

The program below

#include <stdio.h>
void foo(int *x);

int main(void) {
  int i = 5;
  
  printf("In main(): %d\n", i);
  foo(&i);
  printf("In main(): %d\n", i);

  return 0;
}

void foo(int *x) {
  printf("In foo(): %d\n", *x);
  *x = 10;
  printf("In foo(): %d\n", *x);
}

prints

 In main(): 5
 In foo(): 5
 In foo(): 10
 In main(): 10

 Stack PUSH & POP Implementation using Arrays(出栈入栈)

 1 /*STACK PUSH() AND POP() IMPLEMENTATION USING ARRAYS*/
 2 #include <stdio.h>
 3 #include<conio.h>
 4 #define MAX 5
 5 int top, status;
 6  
 7 /*PUSH FUNCTION*/
 8 void push (int stack[], int item)
 9 {   if (top == (MAX-1))
10     status = 0;
11     else
12     {   status = 1;
13     ++top;
14     stack [top] = item;
15     }
16 }
17  
18 /*POP FUNCTION*/
19 int pop (int stack[])
20 {  
21     int ret;
22     if (top == -1)
23     {   ret = 0;
24     status = 0;
25     }
26     else
27     {   status = 1;
28     ret = stack [top];
29     --top;
30     }
31 return ret;
32 }
33  
34 /*FUNCTION TO DISPLAY STACK*/
35 void display (int stack[])
36 {   int i;
37     printf ("\nThe Stack is: ");
38     if (top == -1)
39     printf ("empty");
40     else
41     {   for (i=top; i>=0; --i)
42        printf ("\n--------\n|%3d   |\n--------",stack[i]);
43     }
44     printf ("\n");
45 }
46  
47 /*MAIN PROGRAM*/
48 void main()
49 {  
50     int stack [MAX], item;
51     int ch;
52     clrscr ();
53     top = -1;
54  
55     do
56     {  do
57        {   printf ("\NMAIN MENU");
58        printf ("\n1.PUSH (Insert) in the Stack");
59        printf ("\n2.POP  (Delete) from the Stack");
60        printf ("\n3.Exit (End the Execution)");
61        printf ("\nEnter Your Choice: ");
62        scanf  ("%d", &ch);
63        if (ch<1 || ch>3)
64            printf ("\nInvalid Choice, Please try again");
65     }while (ch<1 || ch>3);
66        switch (ch)
67        {case 1:
68         printf ("\nEnter the Element to be pushed : ");
69         scanf  ("%d", &item);
70         printf (" %d", item);
71         push (stack, item);
72         if (status)
73         {   printf ("\nAfter Pushing ");
74             display (stack);
75             if (top == (MAX-1))
76             printf ("\nThe Stack is Full");
77         }
78         else
79             printf ("\nStack overflow on Push");
80         break;
81        case 2:
82         item = pop (stack);
83         if (status)
84         {    printf ("\nThe Popped item is %d.  After Popping: ");
85              display (stack);
86         }
87         else
88              printf ("\nStack underflow on Pop");
89         break;
90        default:
91         printf ("\nEND OF EXECUTION");
92        }
93     }while (ch != 3);
94 getch();
95 }
push_pop

 引自《C专家编程》

@对于宏这样的预处理器,只应该适量使用,故无需深入讨论。C++在这方面引入了一些新的方法,使得预处理器几乎无用武之地;宏最好用来命名常量

1.9 阅读ANSI C标准,寻找乐趣和裨益

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
foo(const char **p)
{
}
main(int argc, char **argv)
{
    foo(argv);
}
/*
cc test.c 
test.c: In function ‘main’:
test.c:6:5: warning: passing argument 1 of ‘foo’ from incompatible pointer type [enabled by default]
test.c:3:1: note: expected ‘const char **’ but argument is of type ‘char **’
*/
/*
g++ test.c 
test.c: In function ‘int main(int, char**)’:
test.c:8:13: error: invalid conversion from ‘char**’ to ‘const char**’ [-fpermissive]
test.c:5:6: error:   initializing argument 1 of ‘void foo(const char**)’ [-fpermissive]
*/

左定值,右定向

1.ANSI C标准中的两句话
  * 每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象
  (该对象类型不能含有限定符)也就是说参数传递过程类似于赋值
* 要使赋值形式合法,必须要满足下列条件之一: 两个操作数都是指向有限定符或无限定符的相容类型的指针, 左边指针所指向的类型必须具有右边指针所指向类型的全部限定符 正如上句话所言:使得函数调用中实参char*能够与形参const char*匹配,以下代码来说明:
char *cp;
const char *ccp;
ccp = cp;
左操作数是一个指向有const限定符的char的指针
右操作符是一个指向没有限定符的char的指针
char类型与char类型是相容的,左操作数所指向的类型具有右操作符所指向类型的限定符(无),再加上自身的限定符(const)
注意:反过来就不能进行复制: cp = ccp; 将产生编译警告
又有一个标准:const float* 类型并不是一个有限定符的类型--它的类型是"指向一个具有const限定符的float类型的指针",也就是说
const限定符是修饰指针所指向的类型,而不是指针本身
类似地,const char **也是一个没有限定符的指针类型,它的类型是"指向有const限定符的char类型的指针的指针"

2.const是一个C语言的关键字,它限定一个变量不允许被改变。 3.const int num 和 int const num 是一样的,const只修饰其后的变量,而在类型前后没有区别 4.char const *p 可以理解为char const (*p), 即*p是const的,但是p却是可变的 5 例子: int const *p1, p2; --> (*p1)和p2 不可变,但p1却是可变的 int const *const p1, p2; --> p2是const,是前一个const修饰的,*p1也被前一个const修饰, 而p1被后一个const修饰 int *const p1, p2; --> p1是const,(*const p1)是整体,所以const不修饰p2。 6.const在*的左边,则指针指向的变量的值不可变;在*的右边,则指针的指向不可变。简记为“左定值,右定向”。 7.在c中,对于const定义的指针,不赋初值编译不报错 8.强烈建议在初始化时说明指针的指向,防止出现野指针! 实参char *s与形参const char *p相容,而为什么char **argv与形参const char **p不相容呢 1.首先分析下为什么char *s与形参const char *p相容 s指针指向char类型,p指针也指向char类型,只不过无法通过(*p)改变它的值 const char *p1 = char *p2 二者指向类型相同,左边指针拥有右边指针全部限定符,满足赋值条件 2.为什么char **argv与形参const char **p不相容

 附上:容易混乱的const

[cpp] view plaincopyprint?
01.int main(int argc, char* argv[])  
02.{  
03.  
04.    //定义基本类型的const变量,const 位置在哪儿都可以  
05.    const int x = 2,y = 3;  //两个常量  
06.  
07.    //定义一个非const变量  
08.    int z = 3;     //一个普通变量  
09.  
10.    //定义一个指向常的指针  
11.    const int* pc = &x;   //指向常量的指针  
12.  
13.    //定义一个常指针  
14.    int* const cp = &z;   //常指针  
15.      
16.    //定义一个非const指针  
17.    int* pp = &z;    //int 型指针  
18.  
19.    pc = &z; //可以,pc是一个指向常量的指针,不能通过该指针去修改指针所指向的内存空间的值,但是,该指针可以指向别的变量  
20.  
21.    // *pc = 10; //不可以,*pc所指向的地址为const变量,其值不可更改 pc是一个指向常量的指针,不能通过该指针去修改指针所指向的内存空间的值  
22.  
23.    pc = pp; //可以,pc为指针,其值可被改变 pc是一个指向常量的指针,pp是一个普通指针,用一个普通指针给一个指向常量的指针赋值是可以的  
24.    // pp = pc; //用一个指向常量的指针,赋值给一个普通指针,不可以。如果可以,那么就可以通过普通的指针去修改内存的值  
25.  
26.    *cp = x; //可以,通过常指针去修改指针所指向变量的值是可以的  
27.  
28.    // cp = &x; //不可以,cp为常指针,指针的值不能被修改,给常指针赋值是错误的  
29.  
30.    // pp = &x; //不可以,pp是非const指针,原则上来讲给它赋值是可以的,在不同的编译器下有不同的结果  
31.    // pp = pc; //不可以,指向常量的指针不能赋值给一个普通的指针  
32.    pp = cp; //可以,常指针可以赋值给一个普通指针  
33.  
34.    const int * const pp = &a; //双const 既保护指针又保护内存变量  
35.  
36.    return 0;  
37.}  

 C语言专门定义了两个预处理语句:#ifdef 和 #ifndef,用来测试某个名字是否已经定义

#ifndef HDR

#define EDR

/*hdr.h文件的内容放到这里*/

#endif

 

posted @ 2013-08-01 21:03  CoolRandy  阅读(258)  评论(0编辑  收藏  举报