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\n", a, b);
swap(a, b);
printf("after swap, a is: %d, b is: %d\n", 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\n", a, b);
swap(&a, &b); // 这里需要传地址
printf("after swap, a is: %d, b is: %d\n", 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\n", p.name, p.age);
}
void fun(struct People *p) {
printf("people's name is:%s, age is: %d\n", 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\n", a);
sprintf(str, "a is: %d\n", a);
printf("str is: %s", str);
}
输出:
a is: 666
str is: a is: 666
字符空间
任何内存空间,在操作之前都需要知道两个要素:首地址、结束标志(或字节个数)。
字符空间是以 \0 (0x0000 0000)结束的连续内存空间。\0 这个字符不会出现在字符空间,但是可能出现在非字符空间。字符空间有两种限定方式:
const char *p
:常量,不可修改,例如字符串常量。通常用双引号初始化"..."
。char *p
:变量,允许修改,例如字符数组。通常用字符数组初始化char buf[5]
。
void fun(char *p)
{
int i = 0;
while(p[i] != '\0') // 这里也可以直接用 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 *
类型,不可修改。
非字符空间
字符空间固定以 \0
结束,相反,非字符空间没有结束标志,所以在操作的时候,需要另外一个参数:字节数。非字符空间也有两种定义方式:
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\n", 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 *
:字符空间,以\0
结束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\n", a, *a);
fun2(&a);
printf("a is: %x, a's value is: %d\n", 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\n", 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\n", *a);
printf("%s\n", 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\n", p);
free(p); //释放堆空间
char * p2 = fun2();
printf("p is: %s\n", p2);
char * p3 = fun3();
printf("p is: %s\n", 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\n", a);
printf("function main locate at: %p\n", 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\n", fun);
int (*p1)(const char *, ...) = printf;
p1("print by p: hello\n");
int (*myshow)(const char *, ...);
myshow = (int (*)(const char*, ...))printf;
myshow("print by myshow:666\n");
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\n", a);
}
void fun2(int a)
{
printf("printed in fun2(), a is:%d\n", 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
C 语言的函数 - 内存分析的更多相关文章
- 黑马程序员——C语言开门片内存分析
iOS培训,iOS学习---------型技术博客.期待与您交流!------------ 一.各种进制的总结 1.二进制 (1) 在c语言中二进制以0b开头,输出二进制格式没有固定的格式,自定义输出 ...
- strcpy函数内存分析
void strcpy(char* strDest, char* strSrc) { while((*strDest++ = *strSrc++) != '\0'); } 看上面这段代码,只有一条语句 ...
- Go 语言机制之逃逸分析
https://blog.csdn.net/weixin_38975685/article/details/79788254 Go 语言机制之逃逸分析 https://blog.csdn.net/ ...
- C语言内存分析
C语言内存分析 一.进制 概念:进制是一种计数方式,是数值的表现形式 4种主要的进制: ①. 十进制:0~9 ②. 二进制:0和1 ③. 八进制:0~7 ④. 十六进制:0~9+a b c d e f ...
- iOS_05_变量的内存分析、Scanf函数
一.变量的内存分析 1.字节和地址 * 为了更好地理解变量在内存中得存储细节,先来认识一下内存中得”字节“和”地址“. * 内存以字节为单位 * 不同类型占用的字节是不一样的,数据越大,所需的字节数九 ...
- C语言函数调用时候内存中栈的动态变化详细分析(彩图)
版权声明:本文为博主原创文章,未经博主允许不得转载.欢迎联系我qq2488890051 https://blog.csdn.net/kangkanglhb88008/article/details/8 ...
- c语言的内存分析
1. 进制 1. 什么是进制 ● 是一种计数的方式,数值的表示形式 汉字:十一 十进制:11 二进制:1011 八进制:13 ● 多种进制:十进制.二进制.八进制.十六进制.也就是说,同一个 ...
- C语言 mmap()函数(建立内存映射) 与 munmap()函数(解除内存映射)
mmap将一个文件或者其它对象映射进内存.文件被映射到多个页上,如果文件的大小不是所有页的大小之和, 最后一个页不被使用的空间将会清零.mmap在用户空间映射调用系统中作用很大. 条件 mmap()必 ...
- GO语言延迟函数defer用法分析
这篇文章主要介绍了GO语言延迟函数defer用法,较为详细的分析了GO语言的特性与具体用法,并给出了一个比较典型的应用实例,具有一定的参考借鉴价值,需要的朋友可以参考下 本文实例讲述了GO语言延迟 ...
随机推荐
- 解释c# Peek 方法
peek是用来确定你read的文件是否结束了,如果结束了会返回int型 -1 , 举个例子,你可以在输出每一行之前检查一下文件是否结尾,如果没结束就输出此行. StreamReader sr = ne ...
- SpringMVC的工作原理及MVC设计模式
SpringMVC的工作原理: 1.当用户在浏览器中点击一个链接或者提交一个表单时,那么就会产生一个请求(request).这个请求会携带用户请求的信息,离开浏览器. 2.这个请求会首先到达Sprin ...
- 【10】Python urllib、编码解码、requests、多线程、多进程、unittest初探、__file__、jsonpath
1 urllib urllib是一个标准模块,直接import就可以使用 1.1get请求 from urllib.request import urlopen url='http://www.nnz ...
- 【leetcode】1222. Queens That Can Attack the King
题目如下: On an 8x8 chessboard, there can be multiple Black Queens and one White King. Given an array of ...
- 安装c#服务
https://www.cnblogs.com/zmztya/p/9577440.html 1.以管理员身份运行cmd 2.安装windows服务 cd C:\Windows\Microsoft.NE ...
- 记ubuntu sudo无法使用,su密码不对的解决办法
前言 因为我有强制关机的习惯, 然后就杯具了.. ubuntu版本是 16.04 sudo没法使用, su密码不对, 顿时我就慌了 解决方案 1.1.开机点击ESC,进去GUN GRUB界面 1.2. ...
- 解决kaggle邮箱验证不能confirm的问题
感谢这位博主 https://blog.csdn.net/FrankieHello/article/details/78230533
- Android图片上传(头像裁切+原图原样)
下面简单铺一下代码: (一)头像裁切.上传服务器(代码) 这里上边的按钮是头像的点击事件,弹出底部的头像选择框,下边的按钮跳到下个页面,进行原图上传. ? 1 2 3 4 5 6 7 8 9 10 1 ...
- (66)Nginx+lua+Redis开发
一. 概述 Nginx是一个高性能,支持高并发的,轻量级的web服务器.目前,Apache依然web服务器中的老大,但是在全球前1000大的web服务器中,Nginx的份额为22.4%.Nginx采用 ...
- Spring Boot教程(十一) springboot程序构建一个docker镜像
准备工作 环境: linux环境或mac,不要用windows jdk 8 maven 3.0 docker 对docker一无所知的看docker教程. 创建一个springboot工程 引入web ...