zoukankan      html  css  js  c++  java
  • C 语言的函数

    函数基本概念

    Linux 中,函数在内存的代码段(code 区),地址比较靠前。

    函数定义

    C 语言中,函数有三个要素:入参、返回值、函数名,缺一不可。函数使用前必须先声明,或者在使用之前定义。

    函数声明格式如下:

    int test(int a, char *p);
    

    函数定义格式如下:

    int test(int a, char *p)
    {
    	// 干点啥
    	return 666;
    }
    

    函数调用

    char c = 'a';
    int result;
    result = fun(666, &c);
    

    函数的形参和实参,值传递和引用传递

    函数定义时,为了用参数进行操作,为参数预留的占位符就是形参。
    函数调用时,调用方传到函数中的真实参数就是实参。

    函数调用时,传递的是参数的值(实际上就是复制一份内存),而非参数的地址。值传递时,形参的所有改动,都不会影响实参。值传递和引用传递的区别:

    • 值传递会在内存中开辟新空间,复制实参的数据,作为函数的形参。而引用传递则直接把实参的地址传到函数中
    • 值传递时,形参的修改不影响实参。引用传递因为实参和形参都是指针,且指向同一块内存空间,任何改动都会相互影响。

    值传递示例:

    #include <stdio.h>
    
    int swap(int a, int b)
    {
    	int tmp;
    	tmp = a;
    	a = b;
    	b = tmp;
    }
    int main()
    {
    	int a = 1, b = 666;
    	printf("before swap, a is: %d, b is: %d
    ", a, b);
    	swap(a, b);
    	printf("after swap, a is: %d, b is: %d
    ", a, b);
    	return 0;
    }
    

    输出:

    before swap, a is: 1, b is: 666
    after swap, a is: 1, b is: 666
    

    如果要想在调用的函数中修改参数,就必须传参数的地址过去,类似上面的函数可以改为引用传递:

    #include <stdio.h>
    
    int swap(int *a, int *b)
    {
    	int tmp;
    	tmp = *a;
    	*a = *b;
    	*b = tmp;
    }
    int main()
    {
    	int a = 1, b = 666;
    	printf("before swap, a is: %d, b is: %d
    ", a, b);
    	swap(&a, &b); // 这里需要传地址
    	printf("after swap, a is: %d, b is: %d
    ", a, b);
    	return 0;
    }
    

    引用传递可以改变原参数,输出:

    before swap, a is: 1, b is: 666
    after swap, a is: 666, b is: 1
    

    函数的入参

    因为值传递时,需要为实参多开辟一份内存,所以在函数参数占用空间较大时(例如数组、结构体),通常使用引用传递。

    连续空间

    结构体

    对于下面的结构体,通常用引用传递,而不是值传递:

    #include <stdio.h>
    
    struct People {
    	int age;
    	char * name;
    };
    
    void fun2(struct People p) {
    	printf("people's name is:%s, age is: %d
    ", p.name, p.age);
    }
    void fun(struct People *p) {
    	printf("people's name is:%s, age is: %d
    ", p->name, p->age);
    }
    
    int main()
    {
    	struct People p1 = {22, "jack"};
    	fun(&p1); // 推荐
    	fun2(p1);
    }
    

    数组

    C 语言中,用数组做函数的参数时要注意,因为数组名本身就是个表示地址的标签,所以实参是数组时,实际上就是引用传递:

    int arr[10];
    
    int fun(int *p) {}
    

    连续空间只读性

    引用传递时,如果只是想节省内存空间,而不想让调用的函数修改该空间;或者会传递常量指针给函数。这两种情况下,都需要明确把函数声明中的指针用 const 描述。

    编译通过,运行时段错误示例:

    #include <stdio.h>
    
    void fun(char * p)
    {
    	p[0] = 'x'; // 因为传过来的是字符串常量,这里的修改会报 段错误 segmentation fault
    }
    int main()
    {
    	fun("hello");
    	return 0;
    }
    

    只读参数限定示例:

    #include <stdio.h>
    
    void fun(const char * p)
    {
    	p[0] = 'x'; // 因为参数限定为 const,函数内不可修改,否则编译会报错
    }
    int main()
    {
    	fun("hello");
    	return 0;
    }
    

    sprintf 示例

    printf 将格式化字符串打印到标准输出流,而 sprintf 则将格式化字符串输出到变量中,这几个函数及定义可以通过 man 3 sprintf 查看:

           int printf(const char *format, ...);
           int fprintf(FILE *stream, const char *format, ...);
           int sprintf(char *str, const char *format, ...);
           int snprintf(char *str, size_t size, const char *format, ...);
    
    #include <stdio.h>
    
    int main(void) {
        int a = 666;
        char * str;
        printf("a is: %d
    ", a);
        sprintf(str, "a is: %d
    ", a);
        printf("str is: %s", str);
    }
    

    输出:

    a is: 666
    str is: a is: 666
    

    字符空间

    任何内存空间,在操作之前都需要知道两个要素:首地址、结束标志(或字节个数)。

    字符空间是以 (0x0000 0000)结束的连续内存空间。 这个字符不会出现在字符空间,但是可能出现在非字符空间。字符空间有两种限定方式:

    • const char *p:常量,不可修改,例如字符串常量。通常用双引号初始化 "..."
    • char *p:变量,允许修改,例如字符数组。通常用字符数组初始化 char buf[5]
    void fun(char *p)
    {
    	int i = 0;
    	while(p[i] != '') // 这里也可以直接用  while(p[i])
    	{
    		//干点啥
    		i++;
    	}
    }
    

    strlen 示例

    strlen 函数用于统计字符空间中字符的个数,函数语义如下:

    int strlen(const char * str);
    

    可以自己实现一个 strlen:

    int mystrlen (const char *p) {
    	// 错误处理
    	if (p == NULL) return 0;
    	// 内存处理
    	int i = 0;
    	while(p[i])
    	{
    		i++;
    	}
    	return i;
    }
    

    strcpy 示例

    strcpy 用于拷贝字符,函数语义如下:

    void strcpy(char * dest, const char *src);
    

    可见 strcpy 函数的源字符串限定为 const char * 类型,不可修改。

    非字符空间

    字符空间固定以 结束,相反,非字符空间没有结束标志,所以在操作的时候,需要另外一个参数:字节数。非字符空间也有两种定义方式:

    • unsigned char * p:非字符空间,可以读写。
    • const unsigned char * p:非字符空间,只读。

    非字符空间示例

    非字符空间的函数需要两个参数:空间首地址,空间大小,例如:

    void fun(unsigned char *p, int size)
    {
    	int i;
    	for (i = 0; i < size; i++)
    	{
    		// 针对当前字节 p[i] 进行读写操作,然后 i 自增
    	}
    }
    

    void * 形参化指针参数

    定义非字符空间处理函数时,总是想做的尽可能通用,一般就是逐个字节处理。但是调用处理函数的地方可能需要传入各种类型的指针(int、long、struct 等)。C++ 中有模板类,而 C 语言针对这种情况,允许函数声明中用 void * 通配各种参数。通配符非字符空间也有两种定义方式:

    • void * p:非字符空间,可以读写。
    • const void * p:非字符空间,只读。

    通配符接受的参数,在使用前需要强转为具体类型(通常就是无符号字符):

    void fun(void *p, int size)
    {
    	unsigned char * ps = (unsigned char *)p; // 转为字节指针
    	//printf("%s
    ", ps); // 这是个反例,非字符不可当字符串读取,可能出问题
    }
    

    memcpy 函数

    memcpy 函数用于操作非字符空间,可以在 Linux 终端通过 man 3 memcpy 查看语义。

    void *memcpy(void *dest, const void *src, size_t n);
    

    recv 和 send 函数

    这是两个 socket 通信的函数,在 <sys/socket.h> 头文件中声明,函数语义为:

    ssize_t recv(int socket, void *buffer, size_t length, int flags);
    ssize_t send(int socket, const void *buffer, size_t length, int flags);
    

    函数入参的总结

    根据子函数是否具有修改实参的能力,可以分为:

    • 值传递:无法修改
    • 引用传递:可以修改

    字符空间和数据空间的引用类型:

    • char *:字符空间,以 结束
    • void *unsigned char *(推荐用 void *):数据空间,操作时需同时指定字节数

    引用传递时,如果要限制子函数对实参的修改能力,可以加 const 限定:

    • const char *:字符空间
    • const void *:数据空间

    函数返回值

    函数是个代码集合,但是有三个要素:入参,返回值,函数名。

    函数通过入参和返回值实现承上启下的效果。

    函数的执行结果,有两种方式传给调用者:

    • 返回值:函数执行完后,通过 return 将返回值传给调用者。函数返回值是值传递,调用者需创建新变量接收这个值。
    • 入参的指针:入参是指针,函数执行结果放到这个指针所指向的内存

    返回值不是必须的,可以通过指针类型的入参返回数据给调用者。例如:

    int fun1();			//函数返回 int 值
    void fun2(int *); 	//函数接收并直接操作 int 指针,实现跟上面返回值一样的效果
    

    上面两个函数,调用方式如下:

    int a = 0;
    a = fun1();
    fun2(&a);
    

    函数返回值类型

    返回基本数据类型

    基本类型

    函数可以直接返回 int、char、double 等类型。因为是值传递,调用者和子函数各自都有一份返回值的内存空间,所以数据较大(例如 struct 结构体)时,不适合直接返回。

    连续内存空间

    直接返回变量在内存空间中的地址。

    注意:函数返回值是指针时,需要确保其指向地址的合法性!!
    如果返回值在栈中(局部变量),则一定有问题!可以在全局变量区、数据区、堆区。

    int * fun1();		// 函数返回 int 指针
    void fun2(int **p);	// 函数接收 int 指针的指针
    

    完整实例:

    #include <stdio.h>
    
    int * fun1() {
    	int a = 666;
    	//return &a; // 这里有警告,因为返回了局部变量,这块内存空间在子函数执行完后会被回收掉
    	return 666;
    }
    void fun2(int **p) {
    	int a = 888;
    	**p = a; // 直接改值,也可以改指针地址
    }
    int main () {
    	int *a;
    	a = fun1();
    	printf("a is: %x, a's value is: %d
    ", a, *a);
    	fun2(&a);
    	printf("a is: %x, a's value is: %d
    ", a, *a);
    	return 0;
    }
    

    输出:

    a is: 59298a3c, a's value is: 666
    a is: 59298a3c, a's value is: 888
    

    返回连续空间

    注意:函数返回值是指针时,需要确保其指向地址的合法性!!
    如果返回值在栈中(局部变量),则一定有问题!可以在全局变量区、数据区、堆区。

    C 函数中,无法直接返回数组。如果需要返回连续空间,需要返回指针。例如上面的

    int *fun();
    

    就是返回 int 类型的连续空间。

    函数返回指针时,需要注意地址指向的合法性

    返回字符串指针时,需要指向常量区等全局有效的地址。如果当做字符数组,因为是局部变量,会出问题。示例:

    #include <stdio.h>
    
    char * fun3() {
    	//char str[] = "hello"; // 这里创建的字符数组,在子函数执行结束后释放内存,所以返回值的地址非法!!
    	//return str;
    	return "hello"; // 这里创建的字符串常量,存放在内存的常量区,程序执行过程中不会释放
    }
    int main () {
    	char * p = fun3();
    	printf("p is: %s
    ", p);
    	return 0;
    }
    

    输出:

    p is: hello
    

    函数返回值的用法

    要保证子函数执行结束后,子函数中开辟的内存空间不被回收,可以在子函数中创建下面三种类型的数据:

    • 只读数据区:也就是直接返回双引号括起来的常量字符串,注意不要赋值给局部变量,否则还是会被回收
    • 静态数据区:static 修饰的静态数据在程序的生命周期内一直存在
    • 堆区:通过 malloc 在堆中开辟的内存空间,只有在 free 后才会释放

    返回基本类型

    返回基本类型的数据时,因为是值传递,直接用即可:

    int fun() {
    	int a = 666;
    	return a;
    }
    

    如果返回的是基本类型的指针,就需要确保指针的合法性。下面两个例子是反例,因为局部变量的内存空间在函数执行完毕后被释放,所以指针非法,编译时部分编译器会给出警告:

    #include <stdio.h>
    int * fun() {
    	int * a; // 局部变量在程序执行结束后释放
    	int b = 666;
    	a = &b;
    	return a;
    }
    
    char * fun2() {
    	char *str = {"hello"}; // 局部变量在程序执行结束后释放
    	return str;
    }
    
    char * fun3() {
    	static char *str = {"hello"}; // 静态数据区的数据,在程序执行过程中一直有效
    	return str;
    }
    
    int main()
    {
    	int *a = fun();
    	char * s = fun2();
    	printf("%d
    ", *a);
    	printf("%s
    ", s);
       
       return 0;
    }
    

    返回连续内存空间

    前面说了,局部变量在子函数执行完毕后,内存会被释放。返回这个野指针就会出问题。

    为了避免这种情况,可以用 static 修饰局部变量,使其存储在静态区。静态区的数据跟数据区一样,在程序执行时不会释放:

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    char * fun() {
    	char * s = (char *)malloc(100);
    	strcpy(s, "hello");
    	return s; // 只读区的数据在程序执行时不会释放
    }
    char * fun2() {
    	return "hello"; // 只读区的数据在程序执行时不会释放
    }
    char * fun3() {
    	static char str[] = "hello"; // 静态区的数据跟只读区一样,在程序执行时不会释放
    	return str;
    }
    int main () {
    	char * p = fun();
    	printf("p is: %s
    ", p);
    	free(p);	//释放堆空间
    	char * p2 = fun2();
    	printf("p is: %s
    ", p2);
    	char * p3 = fun3();
    	printf("p is: %s
    ", p3);
    	return 0;
    }
    

    输出:

    p is: hello
    p is: hello
    p is: hello
    

    函数名就是标签,指向一段内存

    C 语言中,数组名就是一个标签,指向一段内存。函数名跟数组名类似,也是一个指向一段内存的标签,有对应的地址:

    #include <stdio.h>
    
    int main()
    {
    	int a[3];
    	printf("array a locate at: %p
    ", a);
    	printf("function main locate at: %p
    ", main);
    	return 0;
    }
    

    输出:

    array a locate at: 0x7ffec8099430
    function main locate at: 0x40052d
    

    可以创建指向函数的指针

    数组的地址可以赋值给指针,函数的地址同样也可以传给指针。这里以 printf 为例,库函数的具体定义,可以通过 man 3 printf 查看。

    注意,在创建指向函数的指针时,需要保证参数的一致,否则编译会报错:

    #include <stdio.h>
    
    void fun(int a)
    {
        printf("printed in fun(), a is:%d", a);
    }
    
    int main()
    {
        printf("fun's address is: %p
    ", fun);
        int (*p1)(const char *, ...) = printf;
        p1("print by p: hello
    ");
    
        int (*myshow)(const char *, ...);
        myshow = (int (*)(const char*, ...))printf;
        myshow("print by myshow:666
    ");
    
        int (*p2)(int); // 创建指向函数的指针
        p2 = (int (*)(int))fun; // 将函数的地址转为指针
        p2(666); // 用指针执行函数
    
        int (*p[1])(int);
        p[0] = (int (*)(int))fun;
    
        p[0](888);
    
        return 0;
    }
    

    创建函数数组

    #include <stdio.h>
    
    void fun1(int a)
    {
        printf("printed in fun1(), a is:%d
    ", a);
    }
    void fun2(int a)
    {
        printf("printed in fun2(), a is:%d
    ", a);
    }
    
    int main()
    {
        int (*p[2])(int); // 创建包含两个元素的数组 p,每个元素是都指向函数的指针
        p[0] = (int (*)(int))fun1;
        p[1] = (int (*)(int))fun2;
    
        p[0](888);
        p[1](666);
    
        return 0;
    }
    

    输出:

    printed in fun1(), a is:888
    printed in fun2(), a is:666
    
  • 相关阅读:
    Django-配置Mysql
    Django-manage.py shell命令
    Codeforces 1516B AGAGA XOOORRR
    sitemesh入门教程
    养生好习惯
    解决idea自动导入类String总是导入sun.org.apache.xpath.internal.operations包下的String
    [C#]浅谈协变与逆变
    [C#]跨模块的可选参数与常量注意事项
    [C#]LockBits使用笔记
    1.在校研究生申请软件著作权(学校为第一著作人)
  • 原文地址:https://www.cnblogs.com/kika/p/10851506.html
Copyright © 2011-2022 走看看