程序员C语言快速上手——基础篇(五) - 日理万妓 2022-01-20 08:09 305阅读 0赞 ### 文章目录 ### * 基础语法 * * 简单函数 * * 自定义函数 * 调用函数 * 函数的声明 * 函数的作用域 * 简单函数的小结 * 简单指针 * * 什么是指针 * 如何理解内存 * 指针的使用 * 欢迎关注我的公众号:编程之路从0到1 # 基础语法 # ## 简单函数 ## C语言中的函数其实是多条指令的组合单元。更通俗的说就是许多语句的组合单元。函数的好处是可以让编程结构化,而不是像早期的程序那样写成一坨。另外函数可以复用代码,这使得程序员可以少写大量的重复代码,还使得大型程序可以模块化,多人同时开发。 在国内大量的C语言图书及高校的垃圾教材中,大量的例程永远只使用一个`main`函数,虽然这些教材也讲函数,但明显是为了讲而讲,形而上学的讲,完全缺乏实用性。有过编程经验的朋友都知道,实践工作中,C语言的函数和高级语言的类是多么重要的内容,所有的开发工作就是围绕它们展开的,因此C语言的函数内容,应当引起足够的重视。只有真正做过开发工作的人,回头重学C语言才能真正学明白,真正学以致用。 ### 自定义函数 ### 在上一章我们找到了英文字母大小写转换的规律,但是我们每次使用都需要去运算,显得非常麻烦,大家已经使用过很多标准库的函数,这次我们就将该功能封装成一个我们自己的函数。 #include <stdio.h> // 定义字符转换函数 char convchar(char ch){ if (ch >= 97 && ch <= 122){ // 小写字母范围 return ch - 32; }else if (ch >= 65 && ch <= 90){ // 大写字母范围 return ch + 32; } return -1; // 不是英文字母,返回-1 } int main(void){ char str[] = "hello"; // 循环遍历字符数组,当遍历到字符串结束符就停止 for (int i = 0; str1[i] !='\0'; i++){ printf("%c",convchar(str[i])); } return 0; } 打印结果: HELLO 我们自己编写的`convchar`函数功能非常简单,就是将传入的大写字母变小写,小写字母变大写。现在来看一下定义函数的格式 // 编码风格1 返回值类型 函数名(形式参数){ 函数体 } // 编码风格2 返回值类型 函数名(形式参数) { 函数体 } // 编码风格3 返回值类型 函数名(形式参数) { 函数体 } 以上是最常见的三种编写函数的代码风格,由于C语言规范并不严格,还有许多奇奇怪怪的代码风格,但这里我推荐第一种,比较简洁,易于阅读,且减少代码行数,少了花括号独占的那一行,在函数较多的情况下,可以减少许多花括号的行。 需要注意,函数的返回值和形式参数都是可选的,当有返回值时,必须配合`return`语句返回,当函数没有返回值时,应当使用`void`关键字声明,注意我的措辞,是应当,而不是必须!但在我看来,**任何时候都应该明确你的返回值**,而不是省略什么都不写,这是C语法的缺陷,相当不严谨的地方。当然,这也是历史遗留问题,谁让C语言是编程界的老古董呢。C89中,当省略返回值时,会默认函数的返回值为`int`类型。以下代码是可以正常编译运行的。 #include <stdio.h> main(){ printf("hello world\n"); return 0; } 要注意,没有返回值时应当写上`void`,但没有形式参数时,可以省略,也可以写上`void`,不过建议省略,保持代码简洁明了。 // 无返回值,无形参 void printError(){ printf("this is error!\n"); } // 求和函数。形式参数的声明,与普通变量声明相似,有几个就声明几个 int sum(int a,int b,int c){ return a + b + c; } ### 调用函数 ### #include <stdio.h> int sum(int a,int b,int c){ return a + b + c; } int main(){ // 函数调用,在小括号内传入实际的参数,以替换形式参数 int r = sum(1,2,3); printf("%d\n",r); return 0; } ### 函数的声明 ### 在C语言中,出现了两个概念,**声明**和**定义**。除了`C/C++`,在很多高级语言中,声明和定义基本是等同的,大量不了解C语言的程序员也是这么看待的,那么声明和定义到底是什么,有什么区别呢?先看下面的例子 #include <stdio.h> int main(){ printError(); return 0; } void printError(){ printf("this is error!\n"); } 以上代码在VC编译器等其他一些编译器会直接报错,而在GCC编译器只会报警告,仍可以编译运行。大家会发现,这是因为我们自定义的函数写在`main`函数之后,编译器通常从上往下扫描代码,当扫到`printError()`时,会发现并不认识它,有些人会想当然的认为报错是因为编译器不认识该函数产生的,而实际上是报的**重定义了不同的类型**错误。简单解释一下,当编译器扫描到未定义的函数时,编译器会自以为是的给你进行一个隐式声明,但是编译器并不知道函数的返回值和具体的形式参数啊,这时候它就会简单猜测一下,默认你的返回值是`int`,然后根据你调用函数时传的参数简单分析一下形参的类型,Ok,然后编译器继续往下扫描,这时它发现了你写在后面的`printError`函数,可是刚刚已经给你隐式声明了一次`int printError()`,结果发现你的实现是`void printError()`,和声明对不上,返回值类型不一致,那么你的函数实现当然不被认可,编译器认为你在重新修改函数声明。有些聪明人就想,既然这样,那我定义一个`int printError()`函数,它的返回值刚好就是`int`,这样编译器的隐式声明不就能猜对返回值了吗?不错,具有`int`类型返回值的函数定义,形参又比较简单,编译器确实能帮你成功的隐式声明,但这种小聪明是绝不推荐的。 那GCC为什么只警告不报错呢?这是因为GCC编译器已经是现代编译器中最强大的存在,它具有一定的代码智能优化能力,你的某些错误,它帮你兜了。但这种错误是绝不应该犯的,实际中绝不能写这样的代码。出于某些原因,我们想将函数的具体定义写在`main`函数之后,正确的姿势应当是怎样的呢? #include <stdio.h> // 在main函数之前先声明 void printError(); int main(){ printError(); return 0; } // 在main函数之后再定义 void printError(){ printf("this is error!\n"); } 可以看到,函数声明时是不需要花括号包含的函数体的,只需要包含返回值类型、函数名、形式参数即可。但是要注意,小括号后面的分号是不能省略的。 以上示例就是将函数的声明与定义分开,在实际开发时,这些函数声明也并不是像这样直接写到`main`函数之前的源码中,而是写到头文件中,由于我们还没有讲到头文件,具体内容在后面的部分再说。 ### 函数的作用域 ### 这里简单说说作用域的问题,在函数中声明的变量被称为局部变量,只在当前函数体的范围内可被访问,离开该函数体的范围就不可被访问了。而在函数外的声明的变量,称为全局变量,全局变量在整个程序中都可被访问,也就是所有的源文件,包括`.c`和`.h`文件中都可被访问。具体细节,在后续篇幅中会详细说明 ### 简单函数的小结 ### 1. 函数不能返回数组,因此函数的返回值不能是数组类型。 2. 函数没有返回值时,也应当写上`void`明确返回值类型 3. C语言没有函数重载概念。这意味着C中相同作用域内的函数绝不能同名,哪怕返回值和形参都不同。C语言还没有命名空间的概念,这两者综合一起就是C语言最大缺陷之一。 4. C语言函数的声明与定义是分离的,但是在任何时候都应当先声明再实现。这里声明是指显式声明。意即,当自定义的函数被定义在`main`函数之前时,它同时包含了声明与定义。 **关于形式参数与实际参数的概念理解** 形参就相当于是商店橱窗里的塑胶模特,而你带着女朋友进去试衣服,你的女朋友就成了实参。 C语言的实参与形参之间是值传递,简单说就是值拷贝。在调用函数传参时,实际参数的值被复制了一份,拷贝给形参。因此形参与实参本质上就是两个变量,不可等同,它们仅是值相同。就如一对双胞胎小姐姐,即使长得像,穿着相同的衣服,那也还是两个人。 **改进字符大小写转换函数** 这里将之前的字符大小写转换函数做改进,使之能直接转换字符串的大小写 #include <stdio.h> #include <string.h> /* 当数组作为形参时,不能对其使用sizeof运算符 flags: 值为0时,全部转小写,非0时,转大写 */ void convstr(char ch[], int flags){ for (int i = 0; i < strlen(ch); i++){ if (ch[i] >= 97 && ch[i] <= 122){ if(flags) ch[i] = ch[i] - 32; }else if (ch[i] >= 65 && ch[i] <= 90){ if(!flags) ch[i] = ch[i] + 32; } } } int main(){ char str[] = "Hello,ALICE"; convstr(str,0); printf("%s",str); return 0; } 打印结果: hello,alice 这里需特别注意,在C语言中,凡是数组做函数参数时,都是引用传递,或者说是指针传递。其他基本数据类型做函数参数,包括结构体,都是值传递。网上存在很多错误的言论和资料,一定要明确,在C语言中,数组不存在值传递,这也是为什么不能对做函数参数的数组使用`sizeof`运算的原因所在,因为它会自动退化为指针。 ## 简单指针 ## #include <stdio.h> // 计数器 void counter(int count){ count += 10; printf("count=%d\n",count); } int main(){ int t = 0; counter(t); printf("t=%d\n",t); return 0; } 打印结果: count=10 t=0 如上示例中,`counter`函数为一个计数函数,每次调用都将传入的值加10,可是为什么在函数的外部打印`t`值,它的值没有发生改变呢? ### 什么是指针 ### 在回答什么是指针之前,我认为应当先提问为什么需要指针?如果没有明确的应当重视的理由,大家何必花大力气学习它呢? 要说清这个问题,我们先举一个简单例子: 在生活中,我们往往通过网盘来分享视频,而不是直接将本地硬盘上的视频文件发送给对方,这是因为视频文件通常会非常大,直接通过网络发送十分不便(涉及上行网速和下行网速),效率低下。 一来,在发送大文件时,作为发送者的我们必须一直开着电脑,而不能中断发送,这个过程可能需要一两个小时 二来,当我们需要将本地视频发送给多人时(在不同的时间和地点),我们每次都需要重复上述的缓慢发送过程 最后,如果我们自己制作或者修改视频时,也仍然需要重复以上过程。 现在有了网盘,我们只需将视频文件上层一次到网盘中,生成一个地址链接,每次仅分享这个链接即可。当我们修改了视频,或者增加了新视频,仍然放到以前的链接下,甚至不需告知其他人, 现在我们明白了生活中的道理,再看指针就非常清晰了,C语言中的所谓指针,简单理解其实就是数据在内存中的地址,就相当于以上例子中的链接,有了这个链接,我们就能找到共享的资源。我们需要C语言,需要指针,就是为了这极致的性能和效率,这是除了`C/C++`外的其他高级语言所不具备的。即使是号称继承自C语言的Go语言,它的指针也远没有C指针强大。当然,剑过于锋利,剑法却仍然生疏,未伤人先伤己,这或许是人们畏惧指针的一个原因。 ### 如何理解内存 ### 理解了抽象意义上的指针概念,接下来看看,计算机中的内存又是怎么回事? ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70] 在计算机中,内存就是是一片线性的连续的小格子。每个格子的大小都是1字节,且每个格子都有一个编号,这个编号就被称为内存地址。如同现实生活中的门牌号。 当我们需要往内存中存数据时,操作系统就会随机的从这一片连续小格子中给我们分配一些可用的。例如我们要将上图中左边的信息存入内存,则操作系统会根据我们的要求,给我们分配一段空间,假设给了8个小格子,那么操作系统就会将这段空间的起始地址返回给我们,如`0xff0001`。在存数据时,操作系统只关心两件事,一个是分配给你的起始地址,一个是分配的格子的长度。当`char name[]="Bob";`时,操作系统按数据的类型依次将数据往内存中存放,而数组名`name`即代表地址`0xff0001`,接下来`char age = 28;`,我们这里使用一个字节保存整数28,这时,变量`age`的地址则是`0xff0001`\+`4`。在一段内存中,除了给出首地址,其他地址都是通过这种首地址+偏移量的方式来表示的。因此才说,操作系统只关心起始地址和分配的内存空间长度。如果我们在存放一个学号`int num=1000;`,则地址继续往后偏移。由于`int`类型是4字节,因此变量`num`需要占用四个小格子。每个小格子都存放这个32位整数中的8个位。 理解了内存的原理,那么我们就会明白,为什么几乎所有的编程语言中都指明字符串是不可变对象。如上图,字符串`name`存的是`Bob`,这就相当于一个萝卜一个坑,如果修改为`Bruce`,则会超出原来的格子,从而强占变量`age`的格子,覆盖掉了`age`的值,造成这一片内存区域的混乱。 ### 指针的使用 ### 严格的说,C语言中的指针类型,是指**保存的值是内存地址的变量**。 int num = 10; //声明指针类型变量 int *ptr; //给指针类型变量赋值 ptr = # printf("ptr=%x\n",ptr); printf("num=%d\n",*ptr); 打印结果: ptr=22fe44 num=10 上例中,`int *ptr;`,变量`ptr`就是指针变量,与普通变量的区别就是多了一个星号。实际上如果换种写法大家可能一眼就理解了:`int* ptr;`,将`int`和`*`紧挨,把它们看成一个整体,那这个整体就是指针类型,这样就复合我们最初学习的声明变量的格式了:【数据类型】【变量名】。实际上这样写是可以的,但是千万不要这样写,请将星号和变量紧挨一起,不要和类型挨在一起,虽然这很反直觉,但这确实是C语言的潜规则,当大家都这样写的时候,最好还是遵守规范。这样写并不是心血来潮,确实能避免犯一些错误。 这里还要学习两个运算符 * 取地址运算符 `&` 顾名思义,就是可以获得一个变量在内存中的地址。内存地址是一个4字节的数值,通常用16进制显示,如上例中的`22fe44`,它表示变量`num`的值储存在内存编号为`22fe44`的空间中。 * 间接寻址运算符 `*` 以上第10行代码中的星号是间接寻址运算符,它只能对指针变量使用,表示将该指针变量保存的地址对应的内存中的值取出来。这样说比较绕,换个说法,如果直接将一个内存地址对应的内存中保存的值取出来,这就叫直接寻址,如果是对保存地址的变量使用,这就是间接寻址。使用间接寻址运算符的过程被称为**解引用**。 ![图示][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70 1] 注意,指针变量的右值应当是一个地址,而不能是其他值。因此给指针变量赋值时,先使用取地址符`&`求得变量的地址,然后才将这个地址赋给指针变量,这个过程称为指针指向某某变量。根据指向的目标变量的类型不同,指针变量也应当声明为相应的类型。例如,指向`char`型变量,则应声明为`char *p;`。另一个重要的原则是先初始化,后使用。我们上面的例子是不规范的典型,前面我们已经说过多次,C语言中,变量应当声明后立即初始化。 那么指针变量如何在声明时初始化为零值呢? //指针应在声明同时初始化为NULL int *ptr = NULL; //注意,ptr才是指针变量,而不是*ptr,切记! ptr = # 如果直接访问未初始化的指针,会造成无法预知的后果,这种指针也被称为野指针! 简单指针的小结 1. 声明指针变量时,星号应紧靠指针变量,并在同时初始化为`NULL` 2. 指针变量的值应当是一个地址 3. 声明指针类型时的星号和解引用时的星号意义完全不同,注意区分,不要混淆。声明指针类型时的星号,代表指针类型,解引用时的星号是`间接寻址运算符` 使用指针改进计数器示例 #include <stdio.h> // 形参是声明,这里*表示的是指针类型 void counter(int *p){ //此处*表示解引用 *p += 10; printf("count=%d\n",*p); } int main(){ int num = 10; // 直接初始化 int* ptr = # counter(ptr); printf("num=%d\n",num); return 0; } 实际上指针并不难,很多人觉得难,其实就是因此这里的星号写法设计得不合理,极易造成歧义,如果没有熟练指针的运用,那么星号总会给人别扭的感觉,时而会看错。要想快速熟练掌握指针,要学会经常画内存图,一旦发现迷糊了,赶紧画图,多画几次图,大脑建立了概念,就不会再出错。 # 欢迎关注我的公众号:编程之路从0到1 # ![编程之路从0到1][0_1] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70]: /images/20220120/c548bf4a95b1439d8004f63aa1ed50c3.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70 1]: /images/20220120/db502582a898478984cb999af4085821.png [0_1]: /images/20220120/596d1b39aa6f4325914ebb162c8d1a95.png
相关 程序员C语言快速上手——基础篇(三) 文章目录 小拓展:C语言中int的正确使用姿势 语法基础 表达式 算术运算符 关系运算符 待我称王封你为后i/ 2022年01月23日 00:19/ 0 赞/ 263 阅读
相关 程序员C语言快速上手——高级篇(九) 文章目录 高级篇 结构体 背景 结构体的声明与使用 结构体变量的初始化 太过爱你忘了你带给我的痛/ 2021年12月09日 20:55/ 0 赞/ 339 阅读
还没有评论,来说两句吧...