参考
https://cplusplus.com/reference/clibrary/
1. 概念
函数必须先定义后调用,所以main要放在最后
函数不能嵌套定义,没有闭包函数的概念
1.1 基础概念
1.1.1 形参
形参变量只有在函数被调用时才会分配内存,调用结束后,立即释放内存,所以形参变量之后在函数内部有效
在栈中分配内存
1.2.1 实参
实参可以是常量、变量、表达式(表达式必须有返回值)、函数等,无论实参是何种类型的数据,在调用函数时都必须有确定的值
实参和形参在数量上、类型上,顺序上必须严格一致,可以自动进行类型转换
实参、形参可以同名,但他们之间是相互独立的,互不影响。(实参在函数外部有效,形参在函数内部有效)
1.3.1 函数返回值
有返回值 typename func(){return val};
无返回值 void func(){}; 可以不写return
1.3.1 回调
函数f1调用函数f2的时候,函数f1通过参数给函数f2传递了另一个函数f3的指针,在函数f2执行的过程中,函数f2调用了函数f3,这个过程就叫做callback
这个先被当做指针传入,后面又被回调的函数f3就是回调函数
1.4.1 函数声明
type func(type1 param1,type2 param2);
type func(type1,type2); // 声明时可以不写形参,只写数据类型
1.4.1.1 多文件定义
函数声明写在头文件中,链接阶段 找到函数体
一般函数都放在源文件中,这些源文件都已经提前编译好了,并以静态链接库或动态链接库的形式存在
1.2 变量
数据是放在内存中的,变量(以及指针、引用)是给这块内存起的名字,有了变量就可以找到并使用这份数据
1.2.1 左值/右值
左值:指向内存位置的表达式, 叫做左值表达式(存储在内存中,有明确存储地址(可寻址)的数据)
右值:内存中某些地址的数值(可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据))
1.2.2 全局变量
全局变量存储在内存分区中的全局数据区,这个区域中的数据在程序载入内存后被初始化为0,也就是全局变量默认的初始值为0
全局变量修改后,会影响其他函数
全局变量的作用范围不是从变量定义到该文件结束,在其他文件中也有效
static
静态变量,仅限于当前文件内部调用
内部链接
extern
外部存储变量,用于声明在当前文件中将要用到的其他文件中的变量
外部链接
1.2.3 局部变量
局部变量根据C++标准的不同,可能是随机值,也可能是0
main()
函数中定义的变量也是局部变量
空链接
auto
自动变量,离开定义的函数立即消失
register
寄存器变量,离开定义的函数立即消失
static
静态变量,离开定义的函数仍然存在
1.2.4 作用域
全局变量和局部变量
全局变量:的作用域是全部的文件,包括.c和.h文件
全局变量的作用范围不是从变量定义到该文件结束,在其他文件中也有效
{code...}
单独的代码块也是一个作用域
static 全局变量
作用域变成了当前文件
for
,while
会引入新的作用域,(块级作用域,C/cpp的特性)
注意!!!
int main()
{
printf("%d\n",c); // 这里是访问不到全局变量c的
}
int c=10;
注意2
#include <iostream>
using namespace std;
int a;
void func()
{
a = 10;
}
int main()
{
std::cout << a << endl; // 0
func();
std::cout << a << endl; // 10
return 0;
}
1.3 常量
- 数值常量
- 字符常量/字符串常量
- 枚举常量
常量可以是任何基本的数据类型,可以理解为字面量
常量的值在定义后 不能进行修改
const
修饰的变量,也可以是常量,实际上是只读变量(不能用于switch case)
定义常量
#define
预处理器const
关键字
// #define
#define Variable value // 注意没有分号 可以定义字符型、字符串型的常量
// const
const type variable=value; // 注意有分号,且必须直接赋值
1.4 存储类进阶
https://www.runoob.com/cprogramming/c-storage-classes.html
作用域:块作用域、文件作用域
连接:内部链接、外部链接、空链接
存储时期:静态存储时期、动态存储时期
块作用域:局部变量
文件作用域:全局变量
空链接+块作用域:局部变量
外部链接+文件作用域:全局变量 (其他文件也可以使用)
内部链接+文件作用域:静态全局变量 (只在当前文件使用)
具有文件作用域的变量 --> 具有静态存储时期 (全局变量、静态全局变量)
具有块作用域的静态变量 --> 具有静态存储时期 (静态局部变量)
具有块作用域的普通变量 --> 具有动态存储时期 (普通变量)
static 表明的是内部链接,而不是表明存储时期
// main.cpp
int a=1; // 外部链接+文件作用域:全局变量 (其他文件也可以使用)
static b=2; // 内部链接+文件作用域:静态全局变量 (只在当前文件使用)
int main()
{
int c=3; // 空链接+块作用域:局部变量
}
- 自动存储类 (普通局部变量) (块作用域、空链接)
- 寄存器存储类 (register 局部变量) (块作用域、空链接)
- 具有块作用域的静态存储类 (static 局部变量)(块作用域、内部链接)
- 具有外部链接的静态存储类 (普通全局变量)(文件作用域、外部链接)
- 具有内部链接的静态存储类 (static 全局变量)(文件作用域、内部链接)
1.4.1 初始化注意
https://www.cnblogs.com/iBoundary/p/15014843.html
https://blog.csdn.net/weixin_43609874/article/details/123903650
https://blog.csdn.net/u014552102/article/details/126494557
c语言中,全局变量、静态变量(全局静态变量、局部静态变量)不能用变量赋值,只能用常量赋值
注意:是在C语言中,在C++中是可以的!!!
例子1
int a=10;
int b=a; // initializer element is not constant 表达式必须含有常量值
int main()
{
return 0;
}
例子2
int a=10;
static int b=a; // initializer element is not constant 表达式必须含有常量值
int main()
{
return 0;
}
例子3
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=10;
static int b=a; // ERROR
printf("%d\n",b);
return 0;
}
例子4
下面这个是对的,
&a
是常量,a
是变量
#include <stdio.h>
#include <stdlib.h>
int a=10;
int *b=&a; // initializer element is not constant 表达式必须含有常量值
int main()
{
printf("%d\n",*b);
return 0;
}
2. 赋值与参数引用机制
形参-实参相当于赋值操作
2.1 赋值
不同于下面的参数引用机制(地址传递)
对于数值型
赋值修改,并不会改变内存地址
inplace
操作不会改变内存地址(inplace操作也包括自增自减)
#include <stdio.h>
int main()
{
int a=1;
printf("%p\n",&a); // 000000000061FE1C
a=100;
printf("%p\n",&a); // 000000000061FE1C,地址不变
a+=100
printf("%p\n",&a); // 000000000061FE1C,地址不变
return 0;
}
2.2 参数引用机制/copy
形参与函数内的其他局部变量一样,在进入函数时被创建,退出函数时被销毁
函数内的局部参数、形参,只有在 栈 上分配内存,也就是在函数声明和定义的时候参数没有被创建
2.2.1 值传递
把实际的值复制给函数的形参,不共享内存
值传递只传值 地址互相不干扰、永远不变
#include <stdio.h>
int main()
{
int a=20;
int b=10;
int c=a;
c+=1;
printf("a %d\n",a); // 20
printf("c %d\n",c); // 21
printf("a %d\n",a); // 20
return 0;
}
进阶
想当于复制,内存地址不一样
且修改B不会影响A
#include <stdio.h>
int main()
{
int a=20;
int b=10;
int c=a;
printf("%p\n",&a); // 000000000061FE18
printf("%p\n",&c); // 000000000061FE14
c+=1;
printf("a %p\n",&a); // 000000000061FE18
printf("c %p\n",&c); // 000000000061FE14
return 0;
}
2.2.2 引用传递 (地址传递、指针传递)
形参为指向 实参地址 的指针,当对 形参的指向操作时,就相当于对 实参 本身进行的操作
通过指针传递地址到函数,实现在函数内部操作函数外部的数据
数组、字符串、动态分配的内存,不能通过一个参数直接传递,也就是不能进行值传递,必须通过地址指针传递,也不能进行引用传递
将指针作为形参,地址作为实参进行传递
#include <stdio.h>
#include <string.h>
// 要是返回一个指针,则要用dataType* 的方式进行定义
char *strlong(char* s1,char* s2){
if (strlen(s1)>=strlen(s2)){
return s1;
}
else{
return s2;
}
}
int main(){
char s1[30]="hello";
char s2[30]="world";
char *s3;
s3="ex";
s3=strlong(s1,s2);
printf("%s\n",s3);
return 0;
}
注意事项: 地址传递的地址(地址)依然是副本(深拷贝、地址传递)
https://blog.csdn.net/llm_hao/article/details/108432323 进阶例子,看算法与数据结构dsa-单链表
通过地址(指针)传递可以修改指向的内容,但是指针本身是值传递
(当把一个指针作为参数传递给一个函数或方法时,其实是把指针的副本copy传递给了函数,即把指针的值本身传递到ptr,因此当在函数或方法内部修改指针(注意,不是修改指针所指向的值)时,其实修改的是函数内部指针的副本,而非外部的指针本身)
void func(int *arr1)
{
}
int arr[3]={1,2,3};
func(a);
2.2.3 引用传递和引用符号&的区别
https://blog.csdn.net/llm_hao/article/details/108432323
https://blog.csdn.net/L_fengzifei/article/details/128313713!!!
c语言中没有引用传递
引用传递是基于指针的
引用符号&相当于别名,并不是取地址,即共享一段内存(引用传递数值,会修改原始的数据的)
2.3.4 参数传递扩展 c/python
python的传值方式是按照c++中传指针的方式传值的,即不是引用也不是值。如果对象是可变的,那么操作是在传入对象上操作的,如果是不可变的,那么操作后相当于这个标识符指向了另一个对象
(c++传指针本质上也是值传递,只不过传的是地址值,这时候由于是值传递地址值不会被修改,但是存放在地址里的实参的值是可修改的,这时候对应于python传入对象可变的话相当于就在原对象地址上修改这个对象。如果对象本身不可变,相当于传递的是指针常量,自然不能修改这个对象)
python的赋值
a=1
a=2
# 前后两次id(a)不同
2.3 返回值
注意函数的return的值,与函数声明返回值的类型要对应,否则会发生类型转换
对于返回值是结构体和类对象时(值传递),为了防止局部对象被销毁,也为了防止通过返回值修改原来的局部对象,编译器并不会返回这个对象,而是根据这个对象先创建一个临时对象(匿名对象)以拷贝的方式进行,然后再将这个临时对象返回
2.3.1 返回值与局部变量!!!
https://blog.csdn.net/szm1234/article/details/120864801
https://www.cnblogs.com/xuhj001/p/3436175.html
一般的来说,函数是可以返回局部变量的。 局部变量的作用域只在函数内部,在函数返回后,局部变量的内存已经释放了。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错。但是如果返回的是局部变量的地址(指针)的话,程序运行后会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放了,这样指针指向的内容就是不可预料的内容,调用就会出错。准确的来说,函数不能通过返回指向栈内存的指针(注意这里指的是栈,返回指向堆内存的指针,或指向全局数据区、常量区的地址是可以的)
错误使用局部变量
数组定义在栈上,是局部变量,函数要返回一个数组的地址,但是当函数结束时,局部变量在内存中的数据被释放了,所以是错误的
int* func()
{
int a[3]={1,2,3};
return a;
}
int main()
{
int *b=func();
for (int i=0;i<3;i++)
{
cout<<b[i]<<endl;
}
return 0;
}
解决方法
- 外部初始化数组,然后再传递
- 定义静态局部变量,存储在全局静态数据区的内存,不会因为函数的结束而被释放
- 使用结构体包裹,返回结构体,返回时结构体进行了深拷贝(但是如果是返回的结构体的指针,依然是不行的!!!)
#include <iostream>
using namespace std;
struct Arr
{
int arr[3];
};
int* func1(int *arr)
{
arr[0]=1;
arr[1]=2;
arr[2]=3;
// static int a[3]={1,2,3};
return arr;
}
int* func2()
{
// int a[3]={1,2,3};
static int arr[3]={1,2,3}; // 虽然是静态变量,但是变量arr本身只能在func2中使用
return arr;
}
struct Arr func3()
{
struct Arr arr;
arr.arr[0]=1;
arr.arr[1]=2;
arr.arr[2]=3;
return arr; // 结构体变量arr本身,只能在func中使用
}
int main()
{
// version1 函数外初始化
int arr[3];
int *b1=func1(arr);
// version2 使用静态局部数据
int *b2=func2();
struct Arr b3=func3();
for (int i=0;i<3;i++)
{
cout<<b1[i]<<" "<<b2[i]<<" "<<b3.arr[i]<<endl;
}
return 0;
}
例子2
这个是可以的,因为字符串属于常量,存在常量区,不会因为是局部变量而释放!!!
char *func()
{
char *s="hello world";
return s;
}
int main()
{
char *str=func();
cout<<str<<endl;
return 0;
}
下面这个是不可以的,因为字符串被当成了字符数组,所以是局部变量被放在了栈中,函数结束,内存会被释放掉!!!
char *func()
{
char s[]="hello world";
return s;
}
int main()
{
char *str=func();
cout<<str<<endl;
return 0;
}
例子3
堆内存中的数据由程序员释放,函数结束不会被释放,除非整个程序结束
char *func()
{
char *p=(char *)malloc(sizeof(char)*10);
memset(p,0,10);
for (int i=0;i<10;i++)
{
p[i]=0x20+i;
}
return p;
}
int main()
{
char *p=func();
for (int i=0;i<10;i++)
{
cout<<p[i]<<endl;
}
return 0;
}
例子补充
void func(int *arr, int n) {
arr = (int*)malloc(n * sizeof(int)); // 动态分配内存空间
for (int i = 0; i < n; i++) {
arr[i] = i; // 对内存空间进行赋值
}
}
int main() {
int *arr = NULL;
func(arr, 5); // 调用函数分配内存空间
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 输出内存空间中的值
}
free(arr); // 释放内存空间
return 0;
}
3. 函数范例
返回类型(return_type):一个函数可以返回一个值;不返回值得时候
return_type
是void
参数(parameter_list):形参列表
return_type function_name(parmeter_list)
{
...;
}
3. 函数声明与定义
函数声明会告诉 编译器 函数名称 及 如何调用函数
函数的实际主体可以单独定义
形参的名称并不重要,只有参数的类型是必须的
// 方法1
int max(int num1,int num2)
{
...
}
// 方法2
int max(int num1,int num2); //注意是分号
// 方法3
int max(int,int)
3.1 多文件的函数调用与声明
在A文件中声明和定义,在B文件中调用,则需要在调用函数的文件B的顶部声明函数
static/extern
跨文件调用的时候,要加上extern,虽然有些情况下不写extern仍然可以执行(函数不用extern可以,变量不可以)(因为对于函数声明的extern 加不加都是等价的)
static
修饰静态函数,表示该函数只能在当前源文件中被调用去
4. 函数传递
4.1 函数传递数组
#include <stdio.h>
#include <string.h>
// version 1 这里a变成了指针
void func(int a[],int n)
{
for (int i=0;i<n;i++)
{
printf("%d\n",a[i]);
}
}
// version 2 直接使用指针
void func2(int *a,int n)
{
for (int i=0;i<n;i++)
{
printf("%d\n",a[i]);
}
}
int main()
{
int a[10]={1,2,3};
func(a,sizeof(a)/sizeof(int));
func2(a,sizeof(a)/sizeof(int));
}
二维数组
// version 1
void func(int a[][3],int m,int n)
{
for (int i=0;i<m/n;i++)
{
for (int j=0;j<n;j++)
{
printf("%d,%d,%d\n",a[i][j],*(a[i]+j),*(*(a+i)+j));
}
}
}
// version 2
void func(int (*a)[3],int m,int n)
{
for (int i=0;i<m/n;i++)
{
for (int j=0;j<n;j++)
{
printf("%d,%d,%d\n",a[i][j],*(a[i]+j),*(*(a+i)+j));
}
}
}
int main()
{
int a[2][3]={1,2,3,4,5,6};
func(a,sizeof(a)/sizeof(int),sizeof(a[0])/sizeof(int));
// func2(a,sizeof(a)/sizeof(int));
}
数组与局部变量
注意局部变量作为指针函数的返回值情况 https://www.cnblogs.com/xuhj001/p/3436175.html
函数运行结束后会销毁在他内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们会在后续使用过程中可能引发运行时错误
// 下面的例子,有的编译器直接报错,提示 返回局部变量的指针
int *func()
{
int n=100;
return &n;
}
int main()
{
int *p=func(),n;
n=*p;
printf("%d\n",n);
return 0;
}
这个例子很重要https://blog.csdn.net/szm1234/article/details/120864801
4.2 函数传递结构体
// 值传递
// strcut1=struct2 可以进行赋值,但是相当于副本,二者没有任何关系
void func(struct struct_name struct_param)
{
/*code*/
}
struct struct_name struct_param;
func(strct_param); // 只是传递的副本,并不会修改外部参数,相当于值传递
//地址传递
void func(struct struct_name* struct_param)
{
/*code*/
}
struct struct_name struct_param;
func(&strct_param); // 会修改外部参数,相当于地址传递
#include <stdio.h>
#define MaxSize 10
typedef struct
{
int data[MaxSize];
int length;
}SqList;
void InitList(SqList *L)
{
for (int i=0;i<MaxSize;i++)
{
L->data[i]=0;
}
L->length=0;
}
int main()
{
SqList L; //等价于Struct L
InitList(&L);
for (int i=0;i<MaxSize;i++)
{
printf("%d\t",L.data[i]);
}
printf("\n%d\n",L.length);
return 0;
}
例子2
struct Arr
{
int arr[3];
};
struct Arr func3()
{
struct Arr arr;
arr.arr[0]=1;
arr.arr[1]=2;
arr.arr[2]=3;
return arr;
}
int main()
{
struct Arr b3=func3();
for (int i=0;i<3;i++)
{
cout<<b3.arr[i]<<endl;
}
return 0;
}
例子3
#include <iostream>
using namespace std;
#define FUNDLEN 50
double sum(double x,double y);
double sum2(const struct funds *stan);
double sum3(struct funds stan);
struct funds
{
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(double x,double y)
{
return (x+y);
}
double sum2(const struct funds *stan)
{
return (stan->bankfund+stan->savefund);
}
double sum3(struct funds stan)
{
return (stan.bankfund+stan.savefund);
}
int main()
{
struct funds stan={
"l bank",
20.1,
"w save",
20.2
};
cout<<sum(stan.bankfund,stan.savefund)<<endl;
cout<<sum2(&stan)<<endl;
cout<<sum3(stan)<<endl;
return 0;
}
5.指针与函数
5.1 函数名与函数名地址 – 5.3 函数指针
函数名:可以理解为地址,但是实际上不是地址。函数的首地址,是
void()
类型
&函数名:表示以一个指向该函数这个对象的地址 相当于一个指针(指向一个整体)void(*)()
类型
这两个地址值是相等的
指针函数
将指针作为函数的返回值,指针就是一个地址
见引用传递地址传递
注意局部变量作为指针函数的返回值情况 https://www.cnblogs.com/xuhj001/p/3436175.html函数运行结束后会销毁在他内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们会在后续使用过程中可能引发运行时错误
// 下面的例子,有的编译器直接报错,提示 返回局部变量的指针
int *func()
{
int n=100;
return &n;
}
int main()
{
int *p=func(),n;
n=*p;
printf("%d\n",n);
return 0;
}
这个例子很重要https://blog.csdn.net/szm1234/article/details/120864801
5.3 函数指针 – 还要看
https://stackoverflow.com/questions/9552663/function-pointers-and-address-of-a-function!!!
https://stackoverflow.com/questions/6893285/why-do-function-pointer-definitions-work-with-any-number-of-ampersands-or-as!!!
https://stackoverflow.com/questions/37501982/address-operator-with-pointer-to-member-function!!!
https://blog.csdn.net/L_fengzifei/article/details/128118034
在编译时,每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址,可以利用该指针变量调用函数
函数在内存中具有物理地址,该地址能够赋给指针变量
函数名:可以理解为地址,但是实际上不是地址。函数的首地址,是
void()
类型
&函数名:表示以一个指向该函数这个对象的地址 相当于一个指针(指向一个整体)void(*)()
类型
这两个地址值是相等的
函数名在 非&函数名 的 表达式中被隐式转换成函数指针void(*)()
类型
实际上 下面两种C标准都是可以的:
// 标准1
void func1(char *);
void (*ptr1)(char*);
ptr1=func1;
(*ptr1)("li")
// 标准2
void func2(char *);
void (*ptr2)(char*);
ptr2=func2;
ptr2("li")
例子重要!!!
#include <iostream>
using namespace std;
int func(int x)
{
return x*x;
}
int main()
{
// cout<<"hello world"<<endl;
int (*pfunc1)(int)=func;
int (*pfunc2)(int)=&func;
printf("0x func %#p\n",func); // 0x func 0x4015a4
printf("0x &func %#p\n",&func); // 0x &func 0x4015a4
printf("0x pfunc1 %#p\n",pfunc1); // 0x pfunc1 0x4015a4
printf("0x *pfunc1 %#p\n",*pfunc1); // 0x *pfunc1 0x4015a4
printf("0x func2 %#p\n",pfunc2); // 0x func2 0x4015a4
printf("0x *func2 %#p\n",*pfunc2); // 0x *func2 0x4015a4
cout<<"func:"<<func(10)<<endl; // func:100
cout<<"*func:"<<(*func)(10)<<endl; // *func:100
cout<<"*pfunc1:"<<(*pfunc1)(10)<<endl; // *pfunc1:100
cout<<"pfunc1:"<<pfunc1(10)<<endl; // pfunc2:100
cout<<"*pfunc2:"<<(*pfunc2)(10)<<endl; // *pfunc2:100
cout<<"pfunc2:"<<pfunc2(10)<<endl; // pfunc2:100
return 0;
}
函数指针数组
https://blog.csdn.net/lzh201864031/article/details/129785640
char (*ptr())[5]; // ptr是一个函数指针,指向的函数 返回值 是一个由五个char元素构成的数组 ???
// 区别
char (*ptr)[5]; // ptr是一个普通的指针,指向一个由五个char元素构成的数组
常用函数
字符串/数值型转换
把数字存储为字符串就是存储数字字符
123 -> ‘1’ ‘2’ ‘3’ ‘\0’
字符串转数值型
atoi
字符串转整型
// 下面两种字符串都可以
char s[]="123";
char *s="123";
int var=atoi(s);
printf("%d\n",var);
atof
字符串转整型
// 下面两种字符串都可以
// char s[]="123";
char *s="123.456";
double var=atof(s);
printf("%f\n",var);
数值型转字符串
itoa()
#include<stdlib.h>
int a=10;
char s[10]={0};
itoa(a,s);
printf("%s\n",s)
char a='a';
char buffer[10];
itoa(a,buffer,2); // 2表示转换基数
sprintf
// 整型转字符串
int a=120;
char str[20];
memset(str,0,sizeof(str));
sprintf(str,"%d",a);
printf("%s\n",str);
printf("%d\n",sizeof(str));
exit
exit(0); // 正常退出
exit(1); // 异常退出,里边的参数 1 会传给操作系统,主要不为0就表示异常退出