前要:本笔记为初学者上b站翁恺C语言P82-P98所得笔记。学下来之后,是觉得如大众所说,指针是比较抽象的。然后字符串在学了指针和数组之后觉得还是可以理解的。
注意:笔记中绝大多数内容都与翁恺C语言P82之前的数组,函数等内容有连接,所以没学过那里的可能看不懂。
目录
指针
9.1.1 取地址符 &
9.1.2指针
9.1.3指针的应用
9.1.4指针与数组
9.1.5指针和const
9.2.1指针运算
总结——指针用来干什么:
9.2 动态内存分配
字符串
10.1.1字符串
10.1.2 字符串常量与变量
10.1.3 字符串的输入输出
10.1.4 字符串数组以及程序参数
10.2.1 单字符输入输出,用getchar和putchar
10.2.2字符串函数
指针
9.1.1 取地址符 &
1.&:
def(意思是定义definition,缩写为def,下文均用缩写):取得变量的地址,注意,其操作数必须是变量,不能是常量。
eg.scanf(“%d”,&i):
2.输出地址
printf(“%p”,&i);
注:
1.地址一般默认16进制,结果字母为大写。
2. &i 才表示地址,”,”右侧用 i 无法输出I的地址,会输出i在16进制下的值。
3.%x,%X本用于输出16进制数(大写X结果字母均大写,小写x结果字母均小写),但也可以用于输出地址。与%p区别在于
%x与%X输出时不会刻意填满16位,而%p会,用0填满结果左侧。
如
%x ——65fe1c
%X——65FE1C
%p——000000000065FE1C
3.地址的字节数分析
在不同位数的操作系统,&i所占字节数不同:
32位中为4字节,64位中为8字节,
而Int始终为4字节
那这时我们来看一个问题
为什么 int i;scanf(“%d,i) 编译没有报错?
因为32位时Int的值与地址字节数一致,且若i=6,它就放值进位置6,但位置6不能这样用,运行就不会得到想要的结果,而会得到15个0与一个6:0000000000000006.
4.&能取的和不能取的地址
&不能对没有地址的东西取地址(即必须是个变量) 则&(a+b),&(a+1)均不行。
关于可取地址变量的一些事:
1.相邻变量
地址会从大到小(32位下,逐渐减4,后面默认32位)
2.数组的地址,其单元的地址,相邻(下一个)变量地址三者之间的关系
&[a]=a=&a[0]=&a[1]-4;
注:1.数组名称a可直接做地址!其实就对应a[0]。
2.与相邻变量规则一样,数组下一个变量地址减4.
9.1.2指针
1.总起:
指针def:保存地址的变量
eg.
int i;
int *p=&i;(指针的定义)
注:
1.组成分析:(此处地址指i的地址)
*是一种单目运算符,表示访问地址所对应的变量。
p就是指针,是保存地址的变量。(p只是个起的名字,用什么都行,只是都习惯用p,因为point表示指针;再需要强调的是p本身不是指针,是因为和*一起自己才成指针。)
而*p是保存的地址所对应的变量的值,所以用int 来描述类型
2.大家看到这里不知道是否会疑惑:我能直接用i来做事,为何整出个*p这么绕的东西来搞?
别急,往后看
3.对于指针,个人建议刚学用图解可以更好地理解(地址这一点和变量的组合是比较让人头晕的)
2.先再解释一些结构:
int*p,q;
int *p,q:(这里我隔得很开,其实多少个空格都行)
解释:
1.*可以靠近int ,也可以远离int,意思不变;即上面两个式子意思一样。
2.这里两个式子都只定义了p为指针,想p q均定义为则用int*p,*q;,即在需定义的变量前加*
3.所以指针到底有什么用??
为什么我能直接用i这么简单的东西来做事,为何整出个*p这么绕的东西来搞人心态?
先摆结论:指针的用处体现在函数中。
我们都知道,在C中函数变量处于是一个变量空间中,这个空间是独立的,无法改变函数外的变量(不考虑返回值时),即使重名。那么,当我们用指针这种携带着变量地址的变量,我们便可以通过指针,去访问函数外面的i,进而实现在函数中实现改变函数外的量!
开玩笑的说,在函数中直接用I(指调入i),只能得到i的值(肉体),而无法得到i的灵魂!但指针可以!
4.怎么实操呢?
当我们用以下格式定义函数可以在函数被调用时得到某个变量的地址;
void f(int *p);
而当我们用以下格式在非函数区域输入代码则可以在函数里面通过这个指针去访问外面的i,进而去操作i,实现对它值的改变。
int i=0;f(&i)
举例图解:
i本身为6,用指针p在函数中i被访问,最后变成70
5.关于*与&
a.*的一些注意特点
*p可以做右值也可以做左值,eg.int k=*p;*p=k+1;
这里之所以要提左右值因为一般出现在赋值号左边的是变量,而这里是值,是表达式的结果,在这里是可以的。
b.指针运算符*与&的关系
先摆结论:*与&有类似相反作用的效果。
解释:
1.对比以下四个东西:(由此已可见原因之)
*是一种单目运算符,取出指针所存的地址所对应的变量。
&也是一种单目运算符,取出变量所对应的地址。
p是指针,是保存地址的变量。
*p是被保存的地址所对应的变量的值。
2.看一个问题:*&yptr是否等于&*yptr?(注:这里设计yptr做指针时指向变量y)
看似是,对吗?
*&yptr——*(&yptr)——*(yptr的地址)——得到了那个地址上的变量——yptr(一个普通变量)
&*yptr——&(*yptr)——&(y)——得到y的地址(但不是指针,指针是保存地址的变量)
由此可见不是,但依旧可以看出,&*有类似相反的作用。
9.1.3指针的应用
1.交换两个变量
#includevoid swap (int *pa ,int *pb);int main(){int a=5;int b=6;int *p;*p=0;swap(&a,&b);printf("a=%d,b=%d",a,b);return 0;}void swap (int *pa,int *pb){int t=*pa;*pa=*pb;*pb=t;}
函数本身只能返回一个值,而想多值就得用指针来返回。
2.找数组的max与min
#includevoid minmax (int a[],int len,int *max,int *min);int main(void){int a[]={1,2,3,4,5,6,7,8,9,12,13,14,15,16,17,21,55};int min,max;printf("main sizeof(a)=%lu\n",sizeof(a));printf("main a=%p\n",a);minmax(a,sizeof(a)/sizeof(a[0]),&min,&max);printf("a[0]=%d\n",a[0]);printf("*a=%d\n",*a);printf("min=%d,max=%d\n",min,max);return 0;}void minmax (int a[],int len,int *min,int *max){int i;printf("minmax sizeof(a)=%lu\n",sizeof(a));printf("minmax a=%p\n",a);a[1]=1000;*min=*max=a[0];for (i=1;i<len;i++){if (a[i]*max){*max=a[i];}}}
本身输出结果为1,55
这里函数里面用了一个a[1]=1000,则最大以及函数外的a[0]都会受到影响,说明数组也是指针!(后面会再解释)
3.两个整数作除法
一个套路:让函数返回特殊的不属于有效范围的值来表示出错。
常见的返回-1或0.
但若当任何数值都是可能有效的结果时,就无法判断-1和0表示出错,就得分开返回(且本身需要返回的值就需要不止一个时):
状态用函数的return返回;
实际值用指针参数返回。
好处是容易把返回结果放回if语句的条件中,满足(即返回值为1)则执行if。
#includeint divide(int a,int b,int *pc);int main(void){int a=5;int b=6; int c;if(divide(a,b,&c)){printf("%d/%d=%d",a,b,c);}else {printf("0");}return 0;}int divide(int a,int b,int *pc){int ret=1;if(b==0){ret=0;}else{*pc=a/b;}return ret;}
这里有个点是为何引函数时a,b不去用指针而用值?因为用指针做除法时会出现——/*,所以b变成注释,无效了。(别问我怎么知道的,(˃ ⌑ ˂ഃ ))
*pc=*a/*b;
4.一个指针最常见的错误
定义了指针变量,还没有指向任何变量就开始使用指针(如int p;*p=5,下附图解)。此时指针会指向不该去之地,然后程序就会停下来不干了。结果往往如下图2。
图一
图二
9.1.4指针与数组
1.
书接上回,我们提到数组就是指针,其实,严谨的来说,
def.函数参数表中的数组实际上指针。
也就是说在函数中的数组才是指针。(但仍可以用数组的运算符[]来进行运算)
eg.int a[]=int*a
用sizeof来说就是sizeof(a)==sizeof(int*)
所以下面两组函数原型(参数表中的函数)是等价的。
a.int sum(int *ar,int n);
int sum(int ar[],int n);
b. int sum(*int,int);
int sum(int[],int);
2.使用数组做指针的一些注意事项
a.数组名字本身是指针,可以表达地址;
eg.int a[10];int *p=a; //这里无需用&对a取地址
而数组单元表达的是变量,需要用&取地址
eg.a==&a[0];
b.[]运算符可以对数组做,也可以对指针做。
*a=a[0]
如*a=25;则a[0]=25;
c.数组变量是const的指针,所以不能改变所指
即int b[]等价于int *const b 即b是一个常量指针,只能指向一个已知数组。
9.1.5指针和const
总起:指针本身和所指变量都可能const
1.指针是const(指针不可修改)
表示指针一旦得到了某个变量的地址,就不能指向其他变量。
int *const q=&i;//q是const
此时 a.*q=26;对 b.q++;错
2.所指变量是const(无法通过指针修改变量)
表示不能通过这个指针去修改那个变量(而并非使得该变量变成const,也就是说通过其他渠道还是可以去改变此变量),也可以理解为保证了函数不会改变指针所指的值。
eg
const int*p=&i;
此时
a.*p=26 错//*p是const,无法被赋值去改变对应变量
b.i=26 //对!如上,仍可以通过其他渠道改变变量
c.p=&j;//对
3.所以同时可知,导入函数的指针对应的变量可以是const可以不const
eg.
当void f(const int*x);
int a=15;
f(&a); //可以把非const值放入函数
const int b=a;
f(&b);//也可以const值放入函数中
但b=a+a就不行//因为它是const
4.辨析
int i;
a.const int * p1=&i;
b.int const*p2=&i;
c.int *const p3=&i;
判断哪个(指针还是所指变量)被const的标志是const在*的前还是后,在前表示变量被const,无法通过指针去改变这个变量,如a,b;而在后面则表示指针被const,无法再指向其他变量,如c。
5.const指针的应用
a.当要传递的参数的类型比地址大的时候,用指针能用较少的字符数传递值给参数,const还可以避免函数对外面修改(这点现在了解即可,未来结构中有用)
b.const 数组
const int a[]={1,2,3,4,5,6};
此时数组名字本身已经是const的指针了,这里的const表明的是数组中的每一个单元都是const int。所以必须通过初始化进行赋值。
应用:保护数组值(同理于指针,保护其在函数内部不被破坏)
int sum(const int a[],int length);
9.2.1指针运算
1.加一:
a.指针加一不等于地址值加一,而等于地址值上加上sizeof所对应指针的类型.(对char ,是加1:对int,是加4)
图例
b.而对数组:
*p==a[0];
*(p+1)==a[1];
c.def:本质上,给指针加一表示要让指针指向下一个变量。
eg.
int a[10];
int *p=a;
*(p+1)==a[1];
int n;
*(p+n)==a[n];
注意:如指针不是指向数组这类连续分配的空间,则这种连续计算没有意义。
2.指针运算
a.给指针加减一个整数 (+,+=,-,-=)
b.递增递减 (++,–)
关于“ *p++ ”:
def:取出p所指的那个数来,完事后顺便把移到下一个位置
注:
1).先算*,再算++:这里顺序不同一般顺序,大家注意。
2).常用于数组类的连续空间操作中。
3).在某些cpu中,此可被直接译成一汇编指令。(了解)
4)用处在于简化代码。
c.两个指针相减:
结果是 两个指针地址差/(sizeof()的结果)
即在这两地址有几个这样的类型。
比如a[6]-a[0]等于6。
d.指针有加减无乘除(地址除地址无意义),但可以做比较:
<,,>=,!=都可以对指针做。
def:比较的是它们在内存中的地址。
3.关于0地址
def:我们内存里是有0地址,但0地址往往能乱碰,故指针不应有0值。
故当出现0地址时表示的特殊事:a.返回的指针是无效的;B.指针没有真正初始化;
而 NULL:NULL(必须全大写才有效)是一个预定的符号,表示0地址。
注意:有的编译器不愿意你用0来表示0地址,而NULL通用。
4.指针的类型
a.一条原则
无论指向什么类型,所有指针的大小都一样,因为都是地址。但是指向不同类型的指针是不能直接相互赋值的。这是为了避免用错指针。然后因为类型不同导致赋值异常。(int *p赋了4个位置给char*q对应的i)
b.指针的类型转换:
先摆出个东西:
def:void*表示不知道指向什么东西的指针。
计算时于char*相同。
再来:指针也可以转换类型
eg.int*p=&i;void *q=(void*)p]
是强制让int转换为void吗” />总结——指针用来干什么:
1.需要传入较大数据时用作参数。
2.传入数组后对数组做操作
3.函数返回不止一个结果
4.需要用函数修改不止一个变量
5.动态申请的内存……
9.2 动态内存分配
前要:
在输入数据时,C99可以用变量做数组定义的大小,那C99前呢?
malloc
1.用的方法:
前:#include及int*a;
中:a=(int*)malloc (n*sizeof(int));
最后:还要free(a);(重要,习惯!后面讲原因)
解释:
1.向malloc中申请的空间大小是以字节为单位的(sizeof 里面int表示算4字节为单位,换成其他则根据本身字节数决定,如char为1字节)
2.malloc来自stdib.h 所以前要引
3.中 的a是数组。
4.中 里面的第一个*是指针那个*,第二个*是乘(乘整数n,所以记得设n哟)
5.中:malloc本身返回的结果是void*,所以需要类型转换为自己需要的类型
(free的解释在后面~)
2.如果没空间申请了?
代码:你的程序能给你多大空间
#include#includeint main(void){void *p;int cnt =0;while (p=malloc(100*1024*1024)){cnt++;}printf("分配了%d00MB的空间\n",cnt);free(p);return 0;}
解释:
a.100*1024*1024字节=100Mb,也就是会得出电脑能分多少内存出来。我的电脑输出结果是29400MB,不同电脑结果不同。
b.这里用了while语句,原因是当内存申请失败(也就是内存已经无法再分配时),会返回0,那么while循环结束,反之继续每次循环申请100MB,直到无法申请为止,输出cnt(计数君)。
c.记得最后的最后(当然在return 0;前)补上free(p);! ——下面解释为什么啦!
3.关于free()
def:把申请的来的空间(首地址)还给”系统“——申请过的空间,最终都应该要换。
注:只能还申请来的空间的首地址(也不能还没借的)
关于使用free的常见问题
1.有malloc,就记得free!
看到B站上到这时有个弹幕说:给女朋友改备注叫买脑壳,给自己叫芙锐 ——6
2.申请了没free——长时间这样运行内存会逐渐降低(但不用慌,编程软件如今都会自动释放内存。但是!当你以后做大程序时,这个毛病会让你的程序越搞越卡,所以这终究是个坏习惯,还是得记得free)
同时
新手往往是忘记。
而老手则是找不到合适的free的时机(当代码结构不好时,不好free,所以是不够老的老手~)
所以以后也得注意代码结构的问题。
3.已经free过了再free
4.地址变过了,再去free——一定要是申请空间的首地址 比如定义了p,就是还p(因为p=*p[0])
字符串
10.1.1字符串:
1.字符串
引:char word[];{‘h’,’e’,’l’,’l’,’o’,’!”};
是C的字符数组,但不是C的字符串。要在最后补上一个‘\0′(是整数0),才也是C的字符串。
def:以0(整数0)结尾的一串字符
注:
1. 0或’\0’效果一致,但不同与’0‘;
2. 0标志字符串的结束,但是它不是字符串的一部分。
3.计算字符串长度时不包含这个0;
4.字符串更多以数组形式存在,可以数组或指针形式访问,其中更多以指针形式访问。
2. 字符串变量(1)
以下为分别以指针,数组定义的字符串变量:
char*str =”hello”;
char word[]=”hello”;
char line[10]=”hello”;
3.字符串常量(又称字面量,用于表示固定值的符号,如整数,浮点数,字符串)(1)
“Hello”:“Hello”会被编译器变成一个字符数组放在某处,这个数组的长度是6,因为结尾还有表示结束的‘0’。
4.关于一个规则要强调:
C中字符串只能用双引号“”,不能用单引号’‘。
单引号仅限于单个字符,且暗含没有’\0‘,且如’a’只占一个字节,而“ab”占三个字节(一个字符一个字节)。
总结:
1.C语言的字符串是以字符数组的形式存在的。、
2.不能用运算符对字符串做运算,因为它是数组。
3.通过数组的方式可以遍历字符串。
4.C语言的字符串处理不算先进(因为字符串的大量使用是在当代才开始,而C开发得比较早,当然后面C的延申语言Python,C++都比较先进),唯二的特殊之处是字符串字面量可以用来初始化字符数组以及标准库提供了一系列字符串函数。
10.1.2 字符串常量(2)与变量(2)
1.用指针定义的字符串
char*s=”hello world”;
图解
解释:
a.s是个指针,初始化指向一个字符串常量。
b.用指针定义的常量位为只读区,所以s实际上是const char*s,但由于历史原因,编译器不接受带有const char 的写法。
c.同时和被const的*p一样,对*s所指的字符串不可做写入。
2.而若想修改字符串,应该用数组:
char [s]=”hello world”;
图解
3.那字符串到底用指针还是数组:
先摆结论:
构造一个字符串,用数组;处理一个字符串,用指针。
理由也很简单,
a.用指针定义的字符串,指向只读区,无法再构造,但可以像指针一样用于函数中处理参数,以及动态分配空间。
b.用数组定义的字符串,指向一个非只读区段,它作为本地变量,其空间会被自动回收,方便构造。
4.常见的理解问题
字符串可以表达为char*的形式,而char*不一定是字符串
原因
a.char*本意是指指向字符的数组,可能指向字符的数组,也可能指向单个数组。
b.只有当char*所指的字符数组有结尾的0才说所指为字符串。
10.1.3 字符串的输入输出
1.字符串赋值
s=t;
图解
可见,*s并非新建的字符串。
2.字符串输入输出
def : char string [8];
输入: scanf(“%s”,string);
输出:printf(“%s”,string);
注:scanf的规则是读入一个单词,到空格,tab或回车为止。
然而,scanf是不安全的,因为不知道要读入内容的长度,所以安全的方法是:
在%与s之间插入数字表示最多允许读入 的字符的数量,这个数字应该比数组的大小小1 即如scanf(”%7s”,string);而下一次的scanf就会跟上上一次的结尾。
例如:
代码:(注:printf中##主要用于看清输出的结尾)
#include void f(void) { char word[8]; char word2[8]; scanf("%7s",word); scanf("%7s",word2); printf("%s##%s##\n",word,word2);}int main(void) { f();return 0;}
输入输出对应:
123 123456789——123##1234567##
123456789 123——1234567##9123##
3.空字符串(了解)
a.char buffer[100]=””;
这是个空的字符串,buffer[0]==‘\0’,它是有意义的。
b,char buffer[]=””;
该数组长度只有1;(无法再添加)
10.1.4 字符串数组以及程序参数
1.用何写字符串数组——指针
char*a[] ——指针数组,数组内每个都是指针,指向字符串。
而char**a和char[][](第二个括号有值,如10)都不行,前者中a是个指针,指向另一个指针,那个指针指向一个字符串。后者是二维数组也不行,因为限制了长度。
对比辨析a[][10]与char*a[](图解)
2.程序参数
main里面也有东西:int main(int argc,char const*argv[])
其中,argc表示传入main函数的参数的个数;而argv表示传入main函数的参数数组。当然,此处的argc和argv是可以换成别的名字的,因为argc(argumentcount)和argv(argumentvector)用的比较多,所以建议自己写的代码也用这俩名字。
10.2.1 单字符输入输出,用getchar和putchar
1.getchar:
def:向标准输入(标准指窗口控制台,即编译运行时出来的黑框框,输入时叫标准输入,输出同理)读入一个字符。
eg int getchar(void);
返回类型时Int是为了返回EOF(-1)(来表示输出结束了)
2.putchar:
def:向标准输出写一个字符。
eg. int putchar(int c);
若通过手段返回时写了几个字符,会占用EOF(-1)表示写失败。
以下以计算机原理图释EOF的原理、并说明一些相关快捷键(ctrl+c ,ctrl+d或ctrl+z),重点在于了解计算机的原理中有shell这个中间人
10.2.2字符串函数
总:字符串有很多函数,都藏在string里面,要在开头用#include来调出。
下为五种string中常用的函数:(以下def要配合eg食用,因为def引用了eg的字母)
1.strlen函数(str+len,len表length)
def:返回s的字符串长度(不包含结尾的0)
eg.size_tstrlen (const char*s);
注:a.加const保证函数无法改变char*s,下面同理
b.这里的size_t等同于long long unsigned int(64位系统。非64位少一个long)
2.strcmp函数(str+cmp,cmp表示compare)
def:比较两个字符串,返回
a.0 (当s1=s2)
b.差值s1-s2 (无论s1>s2还是s1<s2)(所以利用返回值时注意s1与s2的顺序)
eg. int strcmp(const char*s1,const char *s2);
3.strcpy 函数(str+cpy,cpy表copy)
def.把src的字符串拷贝到dst并返回dst(返回可以更好的链接代码)
eg. char*s strcpy (char*restrict dst,const char *restrict src);
注:
a.restrict表明src与dst不重叠;(下附图解)
b.是后面的量copy到前面的量中
c.由1.的注,后者用const,前者不用。
对注a图解
当src与dst重叠,拷贝无效,因为现在的多线程计算机运行时会分开copy,因此会出错。(了解即可)
常见应用:复制一个字符串
eg:复制src到dst
char*dst =(char*)malloc(strlen(src)+1);
strcpy(dst,src);
注:
a.要用malloc,因为不知道src的长度。
b.用malloc一定注意strlen(src)后要加一,即别忘了那个‘\0‘。
c.malloc的结果是void*(无方向指针),要强制类型转换为char*。
4.strcat函数(str+cat,cat指catenate 意思是连接,不熟悉单词可以试着用猫的意思做联想:两个猫连在一起走……)
def:把s2拷贝到s1的后面,接成一个长的字符串,并返回s1.其中s1必须有足够多的空间。
eg.char *s strcat(char*restrict det,const char *restrict src);
图例
但是,strcpy和strcat都可能因为空间不够出现安全问题,怎办?
——安全版本:(依次对strcpy,strcat,strcmp,其中cmp被提是有原因的,往下看)
本质:多一个n来控制字符长度上限,类似%ns控制字符串长度。(对于strcpy和strcat)
char*strncpy(char*restrict dst,const char*restrict src,size_t n);
char*strncat(char*restricts1,const char*restrict s2,size_t n);
int strncmp(const char *s1,const char*s2,size_t n);
注:不过对于cmp来说,不是因为安全,而是可以控制字符比较的多少(从左到右)
5.字符串搜索函数(要分类细说,所以标题没有列函数)
a.字符串中找字符:
函数:
第一种: char *strchr(const char*s,int c);
第二种 : char*strrchr (const char*s,int c);
特征:
(假设是从”hello”中找”l”)
第一种,一个r,表示,从左到右找,找到第一个l(也就是代码中的c)后返回llo,也就是从找到的字母到结尾。
第二种,两个r,表示,从右到左找,后面和第一种一样(注意返回时读取顺序还是从左到右,也就是输出lo而不是lleh。(₍˄·͈༝·͈˄*₎◞ ̑̑)
三个使用场景或者技巧:(还是假设从”hello”中找”l”,其中均令p等于第一波函数返回值llo)
1)找第二个l:另设一个strchr,从p+1开始找.
2)输出结果:设个*t(动态保存),strcpy(t,p);
3)输出”l”前的东西,比如本是llo,输出he(注意是从左到右,不过不用硬记,就记这里所有输出都是从左到右就行):找到第一个l后的字母 (这里是l),然后再设一个q=strchr(s,‘l’),然后令c=*q,再令*q=’\0’,然后输出就行啦。注意*q=0指第一个字符变’\0′,因为*q是数组。
下附对3)的图解:
b.字符串中找字符串
总结一下先:
1)strchr ,找单个字符。
2)strstr ,找一个字符串。
3)strcasestr,忽略大小写找一个字符串。
形式:
char *strstr(const char*s1,const char*s2);
char *strcasestr(const char*s1,const char*s2);