ITEEDU

linux下C语言开发常见问题总结

站长原创,版权所有ITEEDU,2011-06-24

笔者于2011年6月26日就得去公司工作,在休息的两天中,闲着无聊,就整理了一些在linux中C语言开发时常遇到的问题,并做以记录。

1.指针参数是如何传递内存的?

如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例1-1中,Test函数
的语句GetMemory(str, 100)并没有使str获得期望的内存,str依旧是NULL,为什么?

  void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}
示例1-1 试图用指针参数申请动态内存
毛病出在函数GetMemory中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p =p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例1-2。
  void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意参数是 &str,而不是str
strcpy(str, "hello"); 
cout<< str << endl;
free(str);
}
示例1-2用指向指针的指针申请动态内存
由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例1-3。
  char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
示例1-3 用函数返回值来传递动态内存
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return语句用错了。这里强调不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例1-4。
  char *GetString(void)
{
char p[] = "hello world";
return p; // 编译器将提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的内容是垃圾
cout<< str << endl;
}
示例1-4 return语句返回指向“栈内存”的指针
用调试器逐步跟踪Test4,发现执行str = GetString语句后str不再是NULL指针,但是str的内容不是“hello world”而是垃圾。
如果把示例1-4改写成示例1-5,会怎么样?
  char *GetString2(void)
{
char *p = "hello world";
return p;
}
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
示例1-5 return语句返回常量字符串
函数Test5运行虽然不会出错,但是函数GetString2的设计概念却是错误的。因为GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。
总结
函数参数的传递分为:值传送和引用(指针)传送。前者将变量的复本传给函数的形参,形参的改变不会引起变量原值得改变;后者将变量的地址传给形参,形参的改变将引起变量的改变。

2.C语言开发常见其他问题总结

(1)再次重申:如果函数的参数是指针,千万不要用该指针申请动态内存。
(2) exit退出整个程序;return退出所在的函数。
(3) 静态链接库的编译

  • cc -c mm.c

ar r libmm.a mm.o//编译静态连接库,2步走
静态连接库是在编译的时候将函数库连接进程序
如何调用静态连接库:
cc -o nn nn.c -L/usr/work/lib -Bstatic -lmm
//其中-Bstatic可以省略

  • cc -dy -G -o libmm.so mm.c

另外,再加上-K PIC后可以提高内存的使用率
//编译动态链接库,它是在程序启动的时候才连接需要的函数
所以利用动态链接库的程序比静态链接库要小得多
需要的环境变量 :(注意:不要忘记设)
LD_LIBRARY_PATH:增加链接程序搜索路径。
LD_RUN_PATH:指定动态链接程序的搜索路径。
例如:LD_LIBRARY_PATH=/usr/work/lib;export LD_LIBRARY_PATH
如何调用libmyfun.so:
cc -o mm mm.c -L/usr/work/lib -Bdynamic -lmyfun
LD_LIBRARY_PATH必须设置,否则运行mm的时候找不到libmyfun.so!
(4)查看动态链接库
察看程序调用了哪些动态链接库利用ldd: 
ldd mm 输出入下:
dynamic linker: nn: file loaded: /usr/work/zzy/libmyfun.so
dynamic linker: nn: file loaded: /usr/lib/libc.so.1
(5) char *str1;
char *str2;
(str1==str)
//这种写法正确,它是指str1和str2指向了同一块内存,即地址相等 ...
if (strcmp(str1,str2)==0)//它是指str1和str2所指向的内存中的内容相等
(6)变量作用域

  • 全局变量和静态全局变量只能在程序的开始初始化一次。非静态局部变量在进入定义

他们的程序块的每个入口多次初始化。静态局部变量也只初始化一次。

  • 静态变量分为:静态全局变量和静态局部变量。
  • 静态全局变量只在他所在的文件中生存,别的文件不可以用extern对其进行声明。
  • 静态局部变量只在两个函数之间跳转,虽然超过作用域,但是这个变量的生存期为整

个程序。
(7)变量的存储方式:extern、register、static、auto。
函数存储默认为extern 
(8)char *str="abc";
printf("%d\n",str);//打印地址
printf("%s\n",str);//打印字符串
(9)初始化数据段。通常将此段称为数据段,它包含了程序中需赋初值的变量。例如,C程序中任何函数之外的说明:
int maxcount = 99;使此变量以初值存放在初始化数据段中。
初始化数据段一般是全局变量和静态变量。
(10)非初始化数据段。通常将此段称为b s s段,这一名称来源于早期汇编程序的一个操作符,
意思是“block started by symbol(由符号开始的块)”,在程序开始执行之前,内核将此段初始化为0。函数外的说明:
long sum[1000] ;
使此变量存放在非初始化数据段中。
(11)栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,
其返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新被调
用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C函数可以递归调用。
(12)堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段顶和栈底之间。
初始化的数据----由exec 赋初值
初始化的数据------由exec从程序文件中读到
正文--------------由exec从程序文件中读到 
注:平常所说的堆栈即为栈(stack)
(13) 函数名: memcpy
功 能: 从源source中拷贝n个字节到目标destin中
用 法: void *memcpy(void *destin, void *source, unsigned n);
他同strcpy的区别就是memcpy可以给结构赋值:

  struct JieGou jg1;
struct JieGou jg2;
memcpy(jg1,jg2,sizeof(struct JieGou));
(14) char *str1="abcd";
此时分配的内存既不在栈中,也不在堆中,而是在静态存储区域。
char str2[]="dfdfdf";
//这里分配的内存在栈中。
(15) 在用delete或用free释放指针p所指的内存后,只是p所指的内存释放了。变量p还是存在的。
应该马上显式地将p置为NULL,以防下次使用p时发生错误。示例程序如下:
if(p==NULL);
strcpy(p,"dkfjdf");//p是个野指针,指向了一堆垃圾
“野指针”不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,
因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。
“野指针”的成因主要有两种:

  • 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,

它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向
合法的内存。例如
char *p = NULL;
char *str = (char *) malloc(100);

  • 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
  • 不要将BOOL值TRUE和FALSE对应于1和0进行编程。大多数编程语言将FALSE定义为0, 任何非0值都是TRUE。Visual C++将TRUE定义为1,而Visual Basic则将

TRUE定义为-1。示例程序如下:

  BOOL flag;

if(flag) { // do something } // 正确的用法
if(flag==TRUE) { // do something } // 危险的用法
if(flag==1) { // do something } // 危险的用法
if(!flag) { // do something } // 正确的用法
if(flag==FALSE) { // do something } // 不合理的用法
if(flag==0) { // do something } // 不合理的用法
  • 小心不要将"= ="写成"=",编译器不会自动发现这种错误。
  • 不要将123写成0123,后者是八进制的数值。
  • 将自己经常犯的编程错误记录下来,制成表格贴在计算机旁边。

(16)以下是我编程时采用的命名约定:

  • 宏定义用大写字母加下划线表示,如MAX_LENGTH;
  • 函数用大写字母开头的单词组合而成,如SetName, GetName ;
  • 指针变量加前缀p,如 *pNode ;
  • BOOL 变量加前缀b,如 bFlag ;
  • int 变量加前缀i,如 iWidth ;
  • float 变量加前缀f,如 fWidth ;
  • double变量加前缀d,如 dWidth ;
  • 字符串变量加前缀str,如 strName ;
  • 枚举变量加前缀e,如 eDrawMode ;
  • 类的成员变量加前缀m_,如 m_strName, m_iWidth ;

对于 int, float, double 型的变量,如果变量名的含义十分明显,则不加前缀,避免烦琐。
如用于循环的int型变量 i,j,k ;float 型的三维坐标(x,y,z)等。
(17)strcpy与memcpy的区别
strcpy(str1,str2)是将str2连带'\0'一同拷贝到str1,比如:str1="aaaa",str2="bbb",
则结果str1="bbb",导致str1碰到'\0',自动终止字符串
memcpy(str1,str2,strlen(str2))是将str2的bbb(不带'\0')拷贝到str1中,结果为
str1="bbba"
这里注意:strlen(str2)=3 sizeof(str2)=4 所以如果不想覆盖str1中其余的字符,
则不能用sizeof。
strncpy与memcpy功能一样。
(18)跨平台申请内存
malloc(sizeof(char)*(strlen(str)+1))
char str[]="hello world";
char str1[9]
这两种情况sizeof(str)=12;sizeof(str1)=9;strlen(str)=11;strlen(str1)=0;
char *p=str;
sizeof (p)=4
如果将str[]用作参数例如fun(char str[100])
则sizeof(str)=4而不是100,因为此时字符串数组自动退化为指针,而指针在内存中占用4个字节。
(19)char *str = "ABCD";
str[0]='M';//将导致错误,因为这种方式是在静态存储区分配内存
如果用strcpy(str,"BBBB")会出错,因为char *str没有动态分配内存,而是指向了静态存储区。
其相对于常量字符串,是不可以修改的。
如果写成:
char *str = (char *)malloc(100)
strcpy (str,"ABCD");
str[0] = 'M';即可
char str[4] = "ABC";
str[0]='N';
则str = "NBC";因为是在栈上分配内存
(20)new 必须与delete对应,动态内存必须要手工释放,动态内存分配的析构函数只会在使用delete的时候被调用。
(21)如果文件中的记录格式为定长,长度为99,则定义:
char *strBuf[101]//99+'\n'+'\0';
fgets(strBuf,101,fp)//从fp中读取100个字符包括\r,都作为一个字符串,最后以'\r'结束(fgets中读取字符串的长度是101-1),这样输出到另一个文件的时候fprint(fp1,"%s",strBuf)自动回车,不用在%s后加\n。
如果用fread(strBuf,100,1,fp)这样的写法即可。
但是之前必须执行memset(strBuf, '\0', sizeof(strBuf)),否则会出现很乱的字符,而fgets不存在这个问题,不知道为什么。总之,在使用字符串之前,最好memset一下。
无论用fgets还是fread读取的文件的记录结果strlen(strBuf)不是99,而是100。
另外:int fread(void *buf, int size, int count,FILE *stream)表示从文件中读取count(字段数)个字段,每个字段大小为size,函数返回实际读取的字段数。如果函数要求的字段数超过实际文件存放的字段数,举例说明:
文件内容如下:
AAAAAAAAAA
AAAAABBBBB
AAAAA

  while(fread(strBuf,11,1,fp))
{
printf("str=[%s]\n",str);
}
如果用fread读文件while(fread(strBuf,11,1,fp))只能读两条出来,前两条返回1,第三条返回0。因为第三条只有6个字节(连着回车符),不满足长度为11的条件,所以fread读文件的话,最实用于定长文件。但是第三条fread也将值赋给了strBuf,只是不满足条件返回了0。第三条的格式:

  • 没有memset(str,'\0',sizeof (str));

[AAAAA(此处一个回车符)
BBBB(此处一个回车符)——这部分是上一个串留下的部分
]

  • 有memset(str,'\0',sizefo(str))

[AAAAA(此处一个回车符)
]
在例如:
AAAAAAAAAA
AAAAA
AAAAAAAAAA
这种情况会跨行截取11个字节,第一条正常,第二条就变为:
[AAAAA(此处是一个回车符)
AAAAA]
如下例所示:

  #include <stdio.h>
#include <stdlib.h>
#include <memory.h>
int main()
{
int i =0;
FILE *fp,*fp1,*fp2;
char str[10]="370706751";
char str1[11];
memset(str1,'\0',sizeof(str1));//将str1用\0填充
fp = fopen("e:\\d_vim\\test.txt", "w");
if (fp == NULL)
{
printf("create file error!\n");
return -1;
}
fprintf(fp, "%s\n\0",str);
fprintf(fp, "%s\n\0",str);
fclose(fp);
fp1 = fopen("e:\\d_vim\\test.txt", "r");
if (fp1 == NULL)
{
printf("create file error!\n");
return -1;
}
fp2 = fopen("e:\\d_vim\\test1.txt", "w");
if (fp2 == NULL)
{
printf("create file error!\n");
return -1;
}
while (fgets(str1,11,fp1) )
{
i++;
printf("%s",str1);//读取test.txt中内容,放入str1中,并在终端显示出来
fprintf(fp2,"%s",str1);//将text.txt中的内容,写入text1.txt中
}
fclose(fp1);
fclose(fp2);//千万勿忘记fclose
printf("record num = [%d]\n",i);
system("pause");
}
运行结果:
370706751
370706751

record num = [2]
而且,在e:\d_vim\test.txt中有两行内容:
370706751
370706751
在e:\d_vim\test1.txt中有两行内容:
370706751
370706751
(22)防止野指针。

 Object object1 = new Object();
......
delete object1;
object1 = NULL;

好的编程习惯,最后置为NULL,防止野指针,因为虽然内存释放了,但是指向一堆垃圾。
(23)memcpy、strncmp和strncpy与memcpy

int main(int argc ,  char *arge[])
{
char str[]=" aaaaaaaaa\0bbbbb";
char str1[]=" aaaaaaaaa";
if (memcmp(str,str1,17) == 0)
printf("equal\n");
if (strncmp(str,str1,17) == 0)
printf("--equal\n");
}
输出结果:--equal

从上例可以看出strncmp和memcmp的区别,strncmp会截断字符串str与str1判断,所以输出相等,但是memcmp会将'\0'带入比较所以不相等。
同理,strncpy与memcpy的区别也一样。

  int main(int argc , char *arge[])
{
char str[18]=" aaaaaaaaa\0bbbbb";
char str2[18];
strncpy(str2,str,17);
for (int i=0; i<17; i++)
{
printf("str2[%d]=[%c]\n", i, str2[i]);
}
memcpy(str2,str,17);
for (int i=0; i<17; i++)
{
printf("str2[%d]=[%c]\n", i, str2[i]);
}
}
memcpy会输出'\0'后面的bbbbb,而strncpy不会输出。
归纳起来,就是说strncpy和strncmp遇到\0会将字符串截断,后续的就不再管了,而memcmp和memcpy遇到\0则将其当作两个字符串来看待哦。