今日学习札记——C语言指针与程序风格(11.6)

来源:互联网 发布:sopcast网络电视下载 编辑:程序博客网 时间:2024/05/18 02:19
1. 指针数组和数组指针
- 指针数组:它的属性是一个数组,数组的元素都是指针。“存指针的数组”
- 数组指针:它的属性示一个指针,它指向一个数组。在32位系统下它永远是4个字节。“指向数组的指针”
例子:int *p1[10];
     int (*p2)[10];
怎样区分它们的定义呢?答案是使用优先级。“[]”的优先级比“*”要高。p1先与“[]”结合,构成一个数组的定义,数组的名为p1,
int*修饰的是数组内的内容。所以这是一个指针数组。
至于p2,这里“()”的优先级比“[]”高,“*”号和p2构成一个指针的定义,指针变量名为p2,int修饰的是数组的内容。数组在这里没有
名字,是个匿名数组。所以它是一个数组指针。

2. 多维数组和多级指针(这里以二级数组和二级指针为例)
- 二维数组,可以看做i,j下标都从0开始的一个矩阵。(按行存储)
# 我们可以以数组下标的方式来访问其中的某个元素:a[i][j];
例如:int Array[Max][Max]={{1,2},{3,4}};
Array[0][1];
# 也可以换算成以指针的方式访问数组元素:*(*(a+i)+j)  //第i+1行,第j+1个
例如:*(*(Array+0)+1)

- 二级指针是经常用到的,即指针的指针(是间接指针)。
例如:char **p;
定义了一个二级指针变量p。p是一个指针变量,在32位系统下占4个byte。二级指针保存的是一级指针的地址,一级指针保存的是数据的地址。
我们可以这样初始化变量p:
1)p =NULL;  //任何指针变量都可以被初始化为NULL
2)char *p2; p = &p2;

3. 数组参数与指针参数 <下钻1:数组参数与指针参数>


4. 怎么对付“野指针”呢
如果我们把内存比作尺子,那尺子上的0毫米处就是内存的0地址处,也就是NULL处。那条拴“野指针”的链子就是这个“NULL”。定义指针变量
的同时最好初始化为NULL,用完指针之后也将指针变量的值设置为NULL。也就是说除了在使用时,别的时间都要把指针“拴”在0地址处,这样就
再不会出错了。


5. 栈、堆、静态区。一般来说,我们可以简单的把内存分为三部分:静态区、栈、堆。
- 静态区:保存自动全局变量和static变量。静态区的内容在整个程序的生命周期内都存在,由编译器在编译的时候分配。
- 栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也就被自动销毁。其特点是效率高,但空间大小有限。
- 堆:由malloc函数和new操作符分配。其生命周期由free和delete决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间较大,
但应注意用完后释放。

6. 常见的内存错误和对策 <下钻2:常见的内存错误和对策>


7. 使用函数的好处:
- 降低复杂性:可以用函数来隐含信息,从而使你不必再考虑这些信息。
- 避免重复代码段:如果在两个不同的函数中的代码很相似,这往往意味着分解工作有误。这时,应该把两个函数中重复的代码都取出来,把公共代码放入
一个新的通用函数中,然后再让这两个函数调用新的通用函数。
- 限制改动带来的影响:由于在独立区域进行改动,因此,由此带来的影响也只局限于一个或几个区域内。
- 进行集中控制:专门化的函数去读取和改变内部数据内容,也是一种集中的控制形式。
- 隐含数据结构:可以把数据结构的实现细节隐含起来。
- 隐含指针操作:指针操作可读性很差,而且很容易引发错误。通过把它们独立在函数中,可以把注意力集中到操作意图而不是集中到指针操作本身。
- 隐含全局变量:参数传递。

8. 编码风格:
- 每一个函数都必须有注释,即使函数短到可能只有几行。
例1:FunctionName、Create Date、Description、Param、Return Code、Global Variable、Revision History
- 函数与函数之间要用一个或若干个空行隔开。
- 在函数体内,定义变量与逻辑语句之间要用空行隔开。
- 逻辑上密切相关的语句之间不应加空行。
- 复杂的函数中,在分支语句,循环语句结束之后需要适当的注释,方便区分各分支或循环体。
例2:while(condition)
{ statement1;
  if (condition)
  { for(condition)
     { statement2;
     } //end for
   } else {
    statement3;
    } //end if
   
  } //end while
- 修改别人代码的时候不要轻易删除别人的代码,应该用适当的注释方式。
例3:while(condition)
    {
      statement1;
      //////////////////////
  //if (condition)
  //{ for(condition)
  //   { statement2;
  //   } //end for
  // } else {
  // statement3;
  // } //end if
  //
  //} //end while      
      //////////////////////
      
      statement4;
      }
- 一行代码的最大长度应控制在80个字符以内,较长的语句、表达式等要分成多行书写。
- 长表达式要在低优先级操作符处划分新行,操作符放在新行之首(以便突出操作符)。
例4:
if((very_longer_variable1 >= very_longer_variable2)
   &&(very_longer_variable3 <= very_longer_variable4)
   &&(very_longer_variable5 <= very_longer_variable6))
{
statement;
 }

for(very_longer_initialization;
very_longer_condition;
very_longer_update)
{
statement;
}

void function( float very_longer_var1,
                 float very_longer_var2,
                 float very_longer_var3)
- 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
例5:int aiMinValue;
    int aiMaxValue;
    int niSet_Value();
    int niGet_value();
- 如果代码行中的运算符较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
例6:leap_year = ((year%4==0)&&(year%100!=0))||(year%400==0)
- 不要编写太复杂的复合表达式
例7:i=a>=b && c<d && c+f <=g+h;
- 尽量避免含有否定运算的条件表达式。
例8:if(!(num>=10))
应改为:if(num<10)
- 参数要书写完整,不要贪图省事只写参数而省略参数名字。
例9:void set_value(int,int);
应改为:void set_value(int width, int height);

9. 好的注释标准
- 注释应当准确,避免二义性。错误的注释不但无益反而有害。
- 边写代码边注释,修改代码同时修改相应的注释,保证注释代码一致性。
- 全局变量必须加以注释。
- 注释的位置应当与被描述的代码相邻,可以与语句在一行,也可以在上行,但不可放在下行。
- 注释代码段时应注重“为何做(why)”,而不是“怎样做(how)”。
- 变量的范围、数值的单位、一系列数字编号要注释。
- 对于函数的入口出口数据给出注释。

---------------<下钻1:数组参数与指针参数>---
1. 一维数组作为参数
我们无法向函数传递一个数组。C语言中,用数组的首地址表示数组,所以传数组就是传一个指针。
在C语言中,所有非数组形式的数据实参均以传值形式(对实参做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,
而只能修改传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上,其开销都是非常大的。所以,理所当然
使用数组的地址值来代表数组了(应传一个指针变量来存放数组初始地址值)。
写法1:
void fun(char *p){       //以指针方式
char c = p[3];
}
写法2:
void fun(char a[]){     //以数组首地址方式
 char c = a[3];
 }

2. 程序示例:
void fun(char *p)
{
char c = p[3];
}
 
int main(){
char *p2 = "abcdefg";
fun(p2);
return 0;
}
我们知道p2是main函数内的一个局部变量,它只在main函数内部有效。这里澄清一个问题,main函数内的变量不是全局变量,而是局部变量,只不过它的生命周期
和全局变量一样长而已。全局变量一定是定义在函数外部的。既然它是局部变量,fun函数肯定无法使用p2的真身,那函数调用就是:对实参做一份拷贝并传递给被调用
的函数。即对p2做一份拷贝,假设其拷贝名为_p2。那传递到函数内部的就是_p2副本。

3. 取得一个内存
方法1:用return
char * GetMemory(char *p, int num){
p = (char*) malloc (num*sizeof(char));
return p;
}
 
int main(){
char *str = NULL;
str = GetMemory(str,10);
strcpy(str,"hello");
printf(str);
free (str);

return 0;
}
方法2:用二级指针
void GetMemory(char **p, int num){
*p = (char*) malloc (num*sizeof(char));
return p;
}

int main(){
char *str = NULL;
GetMomory(&str,10);
strcpy(str,"hello");
printf(str);
free(str);
return 0;
}
 
4. 二维数组和二维指针同样也有两种方法。例如传递二维数组p[3][4]
写法1:void fun(char (*p)[4]);  //这里的括号绝对不能省略,这样才能保证编译器把p解析为一个指向包含4个char类型数据元素的数组。
写法2:void fun(char a[][4]);   //同样作为参数时,一维数组“[]”号内的数字完全可以省略。
---------------end<下钻1:数组参数与指针参数>---


---------------<下钻2:常见的内存错误和对策>---
1. 指针没有指向一块合法的内存
定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。
例1:结构体成员未初始化。
struct student
{
char *name;
int score;
}stu,*pstu;

int main(){
strcpy(stu.name,"Jimmy");
stu.score = 99;
return 0;
}
这里定义了结构体变量stu,但是结构体内部char *name这个成员在定义结构体变量stu时,只是给name这个指针变量分配了4个字节。
name指针并没有指向一个合法的地址,这时候其内部存的只是一些乱码。所以在调用strcpy函数时,会将字符串“Jimmy”往乱码所指
的内存上拷贝,而这块内存name指针根本就无权访问,导致出错。解决办法是为name指针malloc一块空间。stu.name = (char*)malloc(sizeof(char)*6);

例2:接上例。
int main()
{
pstu = (struct student*) malloc (sizeof(struct student));
strcpy(pstu->name,"Jimmy");
pstu->score=99;
free(pstu);

return 0;
}
这里为指针变量pstu分配了内存,但是同样没有为name指针分配内存。错误与上面一样,解决方法也一样。

例3:没有为结构体指针分配足够的内存。
int main()
{
pstu = (struct student*) malloc (sizeof(struct student*));
strcpy(pstu->name,"Jimmy");
pstu->score=99;
free(pstu);

return 0;
}
这里把sizeof(struct student)误写为sizeof(struct student*)。当然name指针同样没有被分配内存。


2. 函数的入口检验。不管什么时候,我们使用指针之前一定要确保指针是有效的。
一般在函数入口处使用assert(NULL!=p)对参数进行校验。在非参数的地方用if(NULL!=p)来校验。但这要求p在定义时被初始化为NULL了。
assert是一个宏,而不是函数,包含在assert.h头文件里。如果其后面括号里的值为假,则程序终止运行,并提示出错(断言为真);如果
后面括号里的值为真,则继续运行后面的代码。这个宏只在Debug版本上起作用,而在Release版本则被编译器优化掉,这样就不会影响代码的性能。

3. 为指针分配的内存太小。导致出现越界错误。
例:char *p1 = "abcdefg";
char *p2 = (char*) malloc(sizeof(char)*strlen(p1)); //只分配7个空间,需要8个
strcpy(p2,p1);
正确写法:
char *p2 = (char*) malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
注意,只有字符串常量才有结束标志符。比如下面这种写法就没有结束标志符:char a[7] = {'a','b','c','d','e','f','g'};


4. 内存分配成功,但并未初始化。
犯这种错误往往是以为内存分配好之后其值自然为0,然而并不是,内存中仍然存的是原来的值。所以在定义变量时,第一件事就是初始化。
例如:
 char *p = (char*) malloc(sizeof(char)*100);
 //memset(p,0,100);         //用menset函数来将内存初始化为0,第一个参数时起始地址,第二个参数是要被设置的值,第三个参数是被设置内存的大小(单位为byte)
 for(int i=0;i<100;i++)
  cout <<p[i]<<" ";


5. 内存越界。这种错误一般是由于操作数组或指针时出现“多1”或“少1”。
例:
int a[10] ={0};
for(i=0;i<=10;i++)
{
 a[i] =i;



6. 内存泄露。如果由malloc函数或new操作符分配的内存,用完之后需要及时free或delete。
- malloc函数。使用语法不细说了。但是,应注意每次都能分配成功吗?不一定。因为:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,
函数返回NULL。这里所说的“堆上剩余内存块”不是所有剩余内存块之和,因为malloc函数申请的是连续的一块内存。因此,我们在使用指向这块内存的指针时,必须
用if(NULL!=p)语句来验证内存块确实分配成功了。
 
- 内存释放。既然有分配,那就有释放,不然的话,有限的内存总会被用光。与malloc对应的就是free函数了。free函数只有一个参数,就是所要释放的内存块的首地址。
例如:free(p);
free到底做了什么事情呢?它只做了一件事,斩断指针变量与这块内存的关系。如在上例中,free函数就是把这块内存和p之间的所有关系斩断,从此p和那块内存之间
再无瓜葛。至于指针变量p本身保存的地址并没有改变,但是它对这个地址处的那块内存已经没有所有权了。
这就是free函数的功能。按照上面的分析,如果对p连续两次以上使用free函数,肯定会发生错误。

- 既然使用free函数之后指针变量p本身保存的地址并没有改变,那我们就需要重新把p的值变为NULL。
例如:p = NULL;


7. 内存已经被释放掉了,但是还在通过指针来使用
- 第一种:就是上面所说的,free(p)之后,继续通过p指针来访问内存。解决的办法就是给p置NULL;
- 第二种:函数返回栈内存。比如在函数内部定义了一个数组,却用return语句返回指向该数组的指针。解决的办法就是弄懂栈上变量的声明周期。
- 第三种:内存使用太复杂,弄不清哪块内存被释放,哪块没有被释放。解决办法是重新设计程序,改善对象之间的调用关系。
---------------end<下钻2:常见的内存错误和对策>---
0 0