C语言进阶(八):函数

本文深入探讨了C语言中的函数,包括函数的意义、参数秘密、函数与宏的区别、递归函数的应用及函数设计原则。讲解了声明和定义的差异、函数参数的求值顺序、调用约定、可变参数的限制,还通过实例分析和编程实验展示了宏的副作用、递归函数设计等。最后强调了函数设计的一系列原则,以帮助读者提升C语言编程能力。
摘要由CSDN通过智能技术生成

目录

一、函数的意义

1、C语言中的函数

2、函数的意义

3、面向过程的程序设计

4、声明和定义

实例分析:声明和定义不同

5、小结

二、函数参数的秘密(上) 

 1、函数参数

实例分析:函数参数的求值顺序

2、程序中的顺序点

3、C语言中的顺序点

编程实验:程序中的顺序点

4、小结

三、函数参数的秘密(下)

1、参数入栈顺序

2、调用约定

实例分析:编写函数计算平均值

3、可变参数

实例分析:编写函数计算平均值

4、变参数的限制

5、小结

四、函数与宏分析

1、函数与宏

编程实验:函数与宏

实例分析:宏的副作用

2、宏的妙用

实例分析:宏的妙用

3、小结

五、递归函数分析

1、递归的数学思想

2、递归函数

3、递归函数设计技巧

4、递归函数设计示例一

编程实验:递归版strlen

5、递归函数设计示例二

编程实验:斐波拉契数列

6、递归函数设计示例三

编程实验:汉诺塔问题求解

7、小结

六、函数设计原则

1、函数设计原则

实例分析:优秀代码赏析

2、课程总结


一、函数的意义

1、C语言中的函数

  • 函数的由来

2、函数的意义

  • 模块化程序设计
  • C语言中的模块化

3、面向过程的程序设计

  • 面向过程是─种以过程为中心的编程思想
  • 首先将复杂的问题分解为一个个容易解决的问题
  • 分解过后的问题可以按照步骤一步步完成
  • 函数是面向过程在C语言中的体现
  • 解决问题的每个步骤可以用函数来实现

4、声明和定义

  • 声明的意义在于告诉编译器程序单元的存在
  • 定义则明确指示程序单元的意义
  • C语言中通过extern进行程序单元的声明
  • 一些程序单元在声明时可以省略extern

严格意义上的声明和定义并不相同!

实例分析:声明和定义不同

#include <stdio.h>

int g_var = 10;

struct Test
{
    int x;
    int y;
};

void f(int i, int j)
{
    printf("i + j = %d\n", i + j);
}

int g(int x)
{
    return (int)(2 * x + g_var);
}
#include <stdio.h>
#include <malloc.h>

extern int g_var;

extern struct Test;

int main()
{
	extern void f(int i, int j);
	extern int g(int x);

	//struct Test* p = (struct Test*)malloc(sizeof(struct Test));// 这里会报错。不能依赖文件的编译顺序
    struct Test* p = NULL;

	printf("p = %p\n", p);

	//g_var = 10;

	printf("g_var = %d\n", g_var);

	f(1, 2);

	printf("g(3) = %d\n", g(3));

	free(p);

	return 0;
}

    struct Test* p = (struct Test*)malloc(sizeof(struct Test));// 这里会报错。不能依赖文件的编译顺序

5、小结

  • 函数是面向过程思想在C语言中的体现
  • 面向过程是由上至下分解问题的设计方法
  • 程序中的定义声明完全不同
  • C语言中通过extern对程序单元进行声明

二、函数参数的秘密(上) 

 1、函数参数

  • 函数参数在本质上与局部变量相同在栈上分配空间
  • 函数参数的初始值是函数调用时的实参值
  • 函数参数的求值顺序依赖于编译器的实现
    .

 

实例分析:函数参数的求值顺序

#include <stdio.h>

int func(int i, int j)
{
    printf("%d, %d\n", i, j);
    
    return 0;
}

int main()
{
    int k = 1;
    
    func(k++, k++);
    
    printf("%d\n", k);
    
    return 0;
}

 

VCC编译器:先运行第二个k++,后运行第一个k++。(其他编译器也可能是先运行第一个)

2、程序中的顺序点

  • 程序中存在一定的顺序点
  • 顺序点指的是执行过程中修改变量值的最晚时刻
  • 在程序到达顺序点的时候,之前所做的一切操作必须完成

3、C语言中的顺序点

  • 每个完整表达式结束时,即分号处
  • &&,||,?:,以及逗号表达式的每个参数计算之后
  • 函数调用时所有实参求值完成后(进入函数体之前)

编程实验:程序中的顺序点

#include <stdio.h>

int main()
{
	int k = 2;
	int a = 1;

	k = k++ + k++; // ((k+k)++)++

	printf("k = %d\n", k);

	if (a-- && a)
	{
		printf("a = %d\n", a);
	}

	return 0;
}

k = k++ + k++; // ((k+k)++)++ 注意:每个编译器可能不同

一条C代码,可能对应很多代码,修改变量值的最晚时刻,就是顺序点。

4、小结

  • 函数的参数在栈上分配空间
  • 函数的实参并没有固定的计算次序
  • 顺序点是C语言中变量修改的最晚时机
     

三、函数参数的秘密(下)

1、参数入栈顺序

函数参数的计算次序是依赖编译器实现的,那么函数参数的入栈次序是如何确定的呢?

strcpy (s, "D.T.Software");

2、调用约定

  • 函数调用发生时
    • 参数会传递给被调用的 函数
    • 返回值会被返回给函数调用者 
  • 调用约定描述参数如何传递到栈中以及栈的维护方式
    • 参数传递顺 序
    • 调用栈清理
  • 调用约定是预定义的可理解为调用协议
  • 调用约定通常用于库调用库开发的时候
    • 从右到左依 次入栈:_stdcall ,_cdecl(C语言默认) ,_thiscall
    • 从左到右依次 入栈︰_pascal,_fastcall

  

实例分析:编写函数计算平均值

#include <stdio.h>

float average(int array[], int size)
{
    int i = 0;
    float avr = 0;
    
    for(i=0; i<size; i++)
    {
        avr += array[i];
    }
    
    return avr / size;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    
    printf("%f\n", average(array, 5));
    
    return 0;
}

  

3、可变参数

  • C语言中可以定义参数可变的函数
  • 参数可变函数的实现依赖于stdarg.h 头文件
    • va_list 一参数集合
    • va_arg一取具体参数值
    • va_start一标识参数访问的开始
    • va_end 一标识参数访问的结束

实例分析:编写函数计算平均值

#include <stdio.h>
#include <stdarg.h>

float average(int n, ...)
{
    va_list args;
    int i = 0;
    float sum = 0;
    
    va_start(args, n);
    
    for(i=0; i<n; i++)
    {
        sum += va_arg(args, int);
    }
    
    va_end(args);
    
    return sum / n;
}

int main()
{
    printf("%f\n", average(5, 1, 2, 3, 4, 5));
    printf("%f\n", average(4, 1, 2, 3, 4));
    
    return 0;
}

  

4、变参数的限制

  • 可变参数必须从头到尾按照顺序逐个访问
  • 参数列表中至少要存在一个确定的命名参数
  • 可变参数函数无法确定实际存在的参数的数量
  • 可变参数函数无法确定参数的实际类型

注意:va_arg中如果指定了错误的类型,那么结果是不可预测的。

5、小结

  • 调用约定指定了函数参数的入栈顺序以及栈的清理方式
  • 可变参数是C语言提供的一种函数设计技巧
  • 可变参数的函数提供了一种更方便的函数调用方式
  • 可变参数必须顺序的访问,无法直接访问中间的参数值

四、函数与宏分析

1、函数与宏

  • 宏是由预处理器直接替换展开的,编译器不知道宏的存在,安全性低(编译器不对参数进行检查)
  • 函数是由编译器直接编译的实体,调用行为由编译器决定
  • 多次使用宏会导致最终可执行程序的体积增大(参数宏的使用会使具有同一作用的代码块在目标文件中存在多个副本
  • 函数是跳转执行的,内存中只有一份函数体存在
  • 宏的效率比函数要高,因为是直接展开,无调用开销
  • 函数调用时会创建活动记录,效率不如宏

编程实验:函数与宏

#include <stdio.h>

#define RESET(p, len)          \
    while( len > 0 )           \
        ((char*)p)[--len] = 0

void reset(void* p, int len)
{
    while( len > 0 ) 
        ((char*)p)[--len] = 0;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int len = sizeof(array);
    int i = 0;
    
    // reset(array,len);
    // RESET(array,len);

    for(i=0; i<5; i++)
    {
        printf("array[%d] = %d\n", i, array[i]);
    }
    
    return 0;
}

 

  • 的效率比函数稍高,但是其副作用巨大
  • 宏是文本替换,参数无法进行类型检查
  • 可以用函数完成的功能绝对不用宏
  • 宏的定义中不能出现递归定义

实例分析:宏的副作用

#include <stdio.h>

#define _ADD_(a, b) a + b
#define _MUL_(a, b) a * b
#define _MIN_(a, b) ((a) < (b) ? (a) : (b))

int main()
{
	int i = 1;
	int j = 10;

	printf("%d\n", _MUL_(_ADD_(1, 2), _ADD_(3, 4)));   // ==> (1+2)*(3+4)
	printf("%d\n", _MIN_(i++, j));                     // ==> 1 < 10 ? 1 : 10

	return 0;
}

与预期不一样,进行单步编译

2、宏的妙用

  • 用于生成一些常规性的代码
  • 封装函数,加上类型信息

实例分析:宏的妙用

#include <stdio.h>
#include <malloc.h>

#define MALLOC(type, x)   (type*)malloc(sizeof(type)*x)
#define FREE(p)           (free(p), p=NULL)

#define LOG_INT(i)        printf("%s = %d\n", #i, i)
#define LOG_CHAR(c)       printf("%s = %c\n", #c, c)
#define LOG_FLOAT(f)      printf("%s = %f\n", #f, f)
#define LOG_POINTER(p)    printf("%s = %p\n", #p, p)
#define LOG_STRING(s)     printf("%s = %s\n", #s, s)

#define FOREACH(i, n)     while(1) { int i = 0, l = n; for(i=0; i < l; i++)
#define BEGIN             {
#define END               } break; } 

int main()
{
    int* pi = MALLOC(int, 5);
    char* str = "D.T.Software";
    
    LOG_STRING(str);
    
    LOG_POINTER(pi);
    
    FOREACH(k, 5)
    BEGIN
        pi[k] = k + 1;
    END
    
    FOREACH(n, 5)
    BEGIN
        int value = pi[n];
        LOG_INT(value);
    END
    
    FREE(pi);
    
    LOG_POINTER(pi);
    
    return 0;
}

3、小结

  • 宏和函数并不是竞争对手
  • 宏能够接受任何类型的参数,效率高,易出错
  • 函数的参数必须是固定类型,效率稍低,不易出错
  • 宏可以实现函数不能实现的功能

五、递归函数分析

1、递归的数学思想

  • 递归是一种数学上分而自治的思想
  • 递归需要有边界条件
    • 当边界条件不满足时,递归继续进行
    • 当边界条件满足时,递归停止

递归将大型复杂问题转化为与原问题相同但规模较小的问题进行处理。

2、递归函数

  • 函数体内部可以调用自己
  • 递归函数
    • 函数体中存在自我调用的函数
  • 递归函数是递归的数学思想在程序设计中的应用
    • 递归函数必须有递归出口
    • 函数的无限递归将导致程序栈溢出而崩溃

3、递归函数设计技巧

  • 递归模型的一般表示法

4、递归函数设计示例一

  • 用递归的方法编写函数求字符串长度

编程实验:递归版strlen

#include <stdio.h>

int strlen_r(const char* s)
{
    if( *s )
    {
        return 1 + strlen_r(s+1);
    }
    else
    {
        return 0;
    }
}

int main()
{
    printf("%d\n", strlen_r("abc"));
    printf("%d\n", strlen_r(""));
    
    return 0;
}

5、递归函数设计示例二

  • 斐波拉契数列递归解法:1,1,2,3,5,8,13,21,...

编程实验:斐波拉契数列

#include <stdio.h>

int fac(int n)
{
    if( n == 1 )
    {
        return 1;
    }
    else if( n == 2 )
    {
        return 1;
    }
    else
    {
        return fac(n-1) + fac(n-2);
    }
    
    return -1;
}

int main()
{
    printf("%d\n", fac(1));
    printf("%d\n", fac(2));
    printf("%d\n", fac(9));
    
    return 0;
}

6、递归函数设计示例三

  • 汉诺塔问题
    • 将木块借助B柱由A柱移动到C柱
    • 每次只能移动一个木块
    • 只能出现小木块在大木块之上
  • 汉诺塔问题分解
    • 将n-1个木块借助C柱由A柱移动到B柱
    • 将最底层的唯一木块直接移动到C柱
    • 将n-1个木块借助A柱由B柱移动到C柱

编程实验:汉诺塔问题求解

#include <stdio.h>

void han_move(int n, char a, char b, char c)
{
    if( n == 1 )
    {
        printf("%c --> %c\n", a, c);
    }
    else
    {
        han_move(n-1, a, c, b);
        han_move(1, a, b, c);
        han_move(n-1, b, a, c);
    }
}

int main()
{
    han_move(3, 'A', 'B', 'C');
    
    return 0;
}

7、小结

  • 递归是一种将问题分而自治的思想
  • 用递归解决问题首先要建立递归的模型
  • 递归解法必须要有边界条件否则无解

六、函数设计原则

1、函数设计原则

  • 函数从意义上应该是一个独立的功能模块
  • 函数名要在—定程度上反映函数的功能
  • 函数参数名要能够体现参数的意义
  • 尽量避免在函数中使用全局变量
  • 当函数参数不应该在函数体内部被修改时,应加上 const声明
  • 如果参数是指针,且仅作输入参数,则应加上 const声明
  • 不能省略返回值的类型
    • 如果函数没有返回值,那么应声明为void 类型
  • 对参数进行有效性检查
    • 对于指针参数的检查尤为重要
  • 不要返回指向“栈内存”的指针
    • 栈内存在函数体结束时被自动释放
  • 函数体的规模要小,尽量控制在80行代码之内
  • 相同的输入对应相同的输出,避免函数带有“记忆”功能
  • 避免函数有过多的参数,参数个数尽量控制在4个以内
  • 有时候函数不需要返回值,但为了增加灵活性,如支持链式表达,可以附加返回值
  • 函数名与返回值类型在语义上不可冲突

实例分析:优秀代码赏析

/*******************************************************************************
 * Copyright (c) 2000, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Kevin Cornell (Rational Software Corporation)
 *******************************************************************************/

/* Eclipse Launcher Utility Methods */

#include "eclipseOS.h"
#include "eclipseCommon.h"
#include "eclipseUtil.h"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#else
#include <unistd.h>
#include <strings.h>
#endif

#define MAX_LINE_LENGTH 256

/* Is the given VM J9 */
int isJ9VM( _TCHAR* vm )
{
	_TCHAR * ch = NULL, *ch2 = NULL;
	int res = 0;
	
	if (vm == NULL)
		return 0;
	
	ch = lastDirSeparator( vm );
	if (isVMLibrary(vm)) {
		/* a library, call it j9 if the parent dir is j9vm */
		if(ch == NULL)
			return 0;
		ch[0] = 0;
		ch2 = lastDirSeparator(vm);
		if(ch2 != NULL) {
			res = (_tcsicmp(ch2 + 1, _T_ECLIPSE("j9vm")) == 0);
		}
		ch[0] = dirSeparator;
		return res;
	} else {
		if (ch == NULL)
		    ch = vm;
		else
		    ch++;
		return (_tcsicmp( ch, _T_ECLIPSE("j9") ) == 0);
	}
}

int checkProvidedVMType( _TCHAR* vm ) 
{
	_TCHAR* ch = NULL;
	struct _stat stats;
	
	if (vm == NULL) return VM_NOTHING;
	
	if (_tstat(vm, &stats) == 0 && (stats.st_mode & S_IFDIR) != 0) {
		/* directory */
		return VM_DIRECTORY;
	}

	ch = _tcsrchr( vm, _T_ECLIPSE('.') );
	if(ch == NULL)
		return VM_OTHER;
	
#ifdef _WIN32
	if (_tcsicmp(ch, _T_ECLIPSE(".dll")) == 0)
#else
	if ((_tcsicmp(ch, _T_ECLIPSE(".so")) == 0) || (_tcsicmp(ch, _T_ECLIPSE(".jnilib")) == 0) || (_tcsicmp(ch, _T_ECLIPSE(".dylib")) == 0))
#endif
	{
		return VM_LIBRARY;
	}
	
	if (_tcsicmp(ch, _T_ECLIPSE(".ee")) == 0)
		return VM_EE_PROPS;
	
	return VM_OTHER;
}

/*
 * pathList is a pathSeparator separated list of paths, run each through
 * checkPath and recombine the results.
 * New memory is always allocated for the result
 */
_TCHAR * checkPathList( _TCHAR* pathList, _TCHAR* programDir, int reverseOrder) {
	_TCHAR * c1, *c2;
	_TCHAR * checked, *result;
	size_t checkedLength = 0, resultLength = 0;
	size_t bufferLength = _tcslen(pathList);
	
	result = malloc(bufferLength * sizeof(_TCHAR));
	c1 = pathList;
    while (c1 != NULL && *c1 != _T_ECLIPSE('\0'))
    {
    	c2 = _tcschr(c1, pathSeparator);
		if (c2 != NULL)
			*c2 = 0;
		
		checked = checkPath(c1, programDir, reverseOrder);
		checkedLength = _tcslen(checked);
		if (resultLength + checkedLength + 1> bufferLength) {
			bufferLength += checkedLength + 1;
			result = realloc(result, bufferLength * sizeof(_TCHAR));
		}
		
		if(resultLength > 0) {
			result[resultLength++] = pathSeparator;
			result[resultLength] = _T_ECLIPSE('\0');
		}
		_tcscpy(result + resultLength, checked);
		resultLength += checkedLength;
		
		if(checked != c1)
			free(checked);
		if(c2 != NULL)
			*(c2++) = pathSeparator;
		c1 = c2;
	}
    
    return result;
}

_TCHAR * concatStrings(_TCHAR**strs) {
	return concatPaths(strs, 0);
}

_TCHAR * concatPaths(_TCHAR** strs, _TCHAR separator) {
	_TCHAR separatorString[] = { separator, 0 };
	_TCHAR * result;
	int i = -1;
	size_t length = 0;
	/* first count how large a buffer we need */
	while (strs[++i] != NULL) {
		length += _tcslen(strs[i]) + (separator != 0 ? 1 : 0);
	}

	result = malloc((length + 1) * sizeof(_TCHAR));
	result[0] = 0;
	i = -1;
	while (strs[++i] != NULL) {
		result = _tcscat(result, strs[i]);
		if (separator != 0)
			result = _tcscat(result, separatorString);
	}
	return result;
}

/*
 * buffer contains a pathSeparator separated list of paths, check 
 * that it contains all the paths given.  Each path is expected to be
 * terminated with a pathSeparator character.
 */
int containsPaths(_TCHAR * str, _TCHAR** paths) {
	_TCHAR * buffer;
	_TCHAR * c;
	int i;
	
	/* terminate the string with a pathSeparator */
	buffer = malloc((_tcslen(str) + 2) * sizeof(_TCHAR));
	_stprintf(buffer, _T_ECLIPSE("%s%c"), str, pathSeparator);
	
	for (i = 0; paths[i] != NULL; i++) {
		c = _tcsstr(buffer, paths[i]);
		if ( c == NULL || !(c == buffer || *(c - 1) == pathSeparator))
		{
			/* entry not found */
			free(buffer);
			return 0;
		}
	}
	free(buffer);
	return 1;
}

int isVMLibrary( _TCHAR* vm )
{
	_TCHAR *ch = NULL;
	if (vm == NULL) return 0;
	ch = _tcsrchr( vm, '.' );
	if(ch == NULL)
		return 0;
#ifdef _WIN32
	return (_tcsicmp(ch, _T_ECLIPSE(".dll")) == 0);
#else
	return (_tcsicmp(ch, _T_ECLIPSE(".so")) == 0) || (_tcsicmp(ch, _T_ECLIPSE(".jnilib")) == 0) || (_tcsicmp(ch, _T_ECLIPSE(".dylib")) == 0);
#endif
}

#ifdef AIX

#include <sys/types.h>
#include <time.h>

/* Return the JVM version in the format x.x.x 
 */
char* getVMVersion( char *vmPath )
{
    char   cmd[MAX_LINE_LENGTH];
    char   lineString[MAX_LINE_LENGTH];
    char*  firstChar;
    char   fileName[MAX_LINE_LENGTH];
    time_t curTime;
    FILE*  fp;
    int    numChars = 0;
    char*  version  = NULL;

	/* Define a unique filename for the java output. */
    (void) time(&curTime);
    (void) sprintf(fileName, "/tmp/tmp%ld.txt", curTime);

    /* Write java -version output to a temp file */
    (void) sprintf(cmd,"%s -version 2> %s", vmPath, fileName);
    (void) system(cmd); 

    fp = fopen(fileName, "r");
    if (fp != NULL)
    {
    	/* Read java -version output from a temp file */
    	if (fgets(lineString, MAX_LINE_LENGTH, fp) == NULL)
    		lineString[0] = '\0';
    	fclose(fp);
    	unlink(fileName);

    	/* Extract version number */
    	firstChar = (char *) (strchr(lineString, '"') + 1);
    	if (firstChar != NULL)
    		numChars = (int)  (strrchr(lineString, '"') - firstChar);
    	
    	/* Allocate a buffer and copy the version string into it. */
    	if (numChars > 0)
    	{
    		version = malloc( numChars + 1 );
    		strncpy(version, firstChar, numChars);
			version[numChars] = '\0';
		}
	}  

    return version;
}

/* Compare JVM Versions of the form "x.x.x..."
 *     
 *    Returns -1 if ver1 < ver2
 *    Returns  0 if ver1 = ver2 
 *    Returns  1 if ver1 > ver2
 */     
int versionCmp(char *ver1, char *ver2)
{
    char*  dot1;
    char*  dot2;
    int    num1;
    int    num2;

    dot1 = strchr(ver1, '.');
    dot2 = strchr(ver2, '.');

    num1 = atoi(ver1);
    num2 = atoi(ver2);

    if (num1 > num2)
    	return 1;
    	
	if (num1 < num2)
		return -1;
	
	if (dot1 && !dot2)   /* x.y > x */
        return 1;

    if (!dot1 && dot2)   /* x < x.y */
        return -1;
    
    if (!dot1 && !dot2)  /* x == x */
        return 0;

    return versionCmp((char*)(dot1 + 1), (char*)(dot2 + 1) );
}
#endif /* AIX */

2、课程总结

  • C语言的学习需要勤思考勤动手才能得到提高
  • 难点部分为指针的学习
    • 指针的本质,指针的运算,指针和数组
  • 学习过程可以采用各个击破的方法
    • 在一个特定的时间段只重点学习和练习某个主题
  • 在熟练掌握C语言的各个特性后再进行项目练习
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值