“鬼鲛,你,怕死?”
纹路
取地址运算:&运算符取得变量的地址
C语言给出了一个工具——sizeof
- sizeof是一个运算符,给出某个类型或变量在内存中所占据的字节数
- sizeof(int)
- sizeof(i)
#include
int 占了4个字节,对应32个bit;double 占了8个字节,对应64个bit(一个字节=8bit)
运算符&
- scanf("%d",&i);里的&是一个运算符。和加减乘除一样。
- 他能够获得变量的地址,它的操作数必须是变量
- 地址的大小取决于架构,取决于编译器
- int i;printf("%p",&i);
&不能取的地址
- &不能对没有地址的东西取地址
比如
&(a+b)
&(a++)
&(++a)
它必须对一个明确的变量取地址。
试试这些&
- 变量的地址
- 相邻的变量的地址
- &的结果的sizeof
- 数组的地址
- 数组单元的地址
- 相邻的数组单元的地址
#include
4c和48的差别在哪呢?
i是先定义的变量,p是后定义的变量。i在更高的地方,48在更低的地方。他们分配一种叫做“堆栈”的地方。在“堆栈”里,我们分配内存是自顶向下来分配的。所以,我们先写的那个变量地址更高,后写的那个变量地址更低,但是他们是紧挨着的,他们的差距为4。这个4就是sizeof(int)。
#include
前三排数字一样说明了:&a=a=&a[0]。我们还发现前三个尾数都是20。其次&a[1]与他们的差距是4。如果我们一直看下去,就会发现,相邻数组间距永远是4。
指针:指针变量就是记录地址的变量
前面看了取地址符,这么多变化,可以取这个,可以取那个,可以看到数组里那些变量的地址,看到那些单元的地址怎么排列的。可是,有什么用呢?我们总要拿他做些什么。
如果我们能够取得一个变量的地址,然后把这个地址作为一个值传给某个函数。那么在那个函数里面能不能通过那个地址访问外面的那个变量呢?
你想想scanf是怎么做事情的。scanf也就是一个函数对不对?
我们在scanf的时候,传给了变量一个地址,在scanf里头,它就有办法拿我们传进去的这个地址,把它从标准输入分析出来的那个整数,或者double,或者char,放到我们所指定的那个变量的里头去。这件事情它是怎么做的,我们能不能照着这个样子,做我们自己的东西?所以scanf里面一定有一个办法,它的函数原型一定有办法接受到这个地址。我们前面试过,如果你把一个地址交给一个整数,这个事情不靠谱,因为整数和地址,永远不见得是相同的类型。那么什么样的类型可以接收取地址得到的那个地址呢?
指针
- 就是保存地址的变量
int i;
int* p=&i; //这个*号在这,表示p是一个指针,它指向一个int,也就是p是i里面的那个地址,现在呢,我们把i这个地址交给了这个p(因为英文中的point代表指出,所以我更常用p来代表一个指针)
int* p,q; //这个*它可以靠近int,也可以远离int靠近变量,但是这一行与下一行所表示的意思是一样的。它们都表示说,p是一个指针,指向一个int。而q是什么?q只是一个普通的int类型的变量。所以换句话说,我们并不是把*加给了int而是加给了p。
int *p,q;
指针变量
- 变量的值是内存的地址
- 普通变量的值是实际的值
- 指针变量的值是具有实际值的变量的地址
作为参数的指针
- void f(int *p);
- 在被调用的时候得到了某个变量的地址:
- int i=0;f(&i);
- 在函数里面可以通过这个指针方向访问外面的这个i
来看下面这样一段代码
#include
这就相当于在main里面我们有一个变量i,放了6,它的地址是70,然后我们把这个地址取出来,交给了另外一个f函数里面的一个变量p,这个p呢,它的值是70,于是我们可以说p是一个指针,它指向了i这个变量。那么,有了这件事情之后,在f函数里面,我们有外面的,main里面的i的地址了,我们不知道它叫做i,但是,我们有它的地址了。如果不是这样子传一个地址进去,我们只能得到它的一个值。它跟那个地址没有任何关系,这件事情我们在学函数的时候就遇到了。我们通过指针变量p得到了i的地址,这使得f函数里面拥有能够访问外面那个i的能力了,那么怎么访问他呢?访问,意味着读或者写,读是访问,我们要读到6这个值,写呢?我们想要改那个变量的值,怎么做?
访问那个地址上的变量*
- *是一个单目运算符,用来访问指针的值所表示的地址上的变量
- 可以做右值也可以做左值
- int k=*p;
- *p=k+1;
#include
我们下面做一件更邪恶的事:
void
结果为:*p=6;k=26;
这意味着在经过f函数的调用后,i的值被改了,我们在学函数的时候,经常提到,C语言的函数调用的时候发生的参数的转移,那是一种值的传递,我们把值传进函数里面去了,所以在函数里面,函数的参数和调用的它的地方没有任何联系,现在情况有点不一样,我们仍然坚持说,在这个时候发生的是值的传递(地址值传进了函数,这仍然是值的传递)因为传进来的是地址,所以通过这个地址在函数内部,我们可以以这种方式去访问到外面的这个i的变量,因为p的值就是i的地址,*p就代表了那个i,这样我们就可以去修改这个i。上面的代码里,对*p=26实际就是对i的修改。——这就是指针。
整数和你的地址是一样大的,你把一个整数传进去和你把一个地址传进去对于scanf来说没有什么区别,它以为你传进去的i是i的地址。它以为你传进去了一个地址,其实你传进去的是6,它不知道,它以为6是一个地址,然后拿那个地址来做事情,所以编译不一定会报错,但运行一定会报错。运行一定会出错是因为scanf把它读进来的那个数字写到了不该写的地方,它没有写到你i里头去,它写到了别的地方去了,比如写到6那个地方去了,那个地方却又很小。
一小段总结:指针就像一根线,把一个一个函数串在了一起;而值(普通变量)它仅适用于单个函数,当他传到令一个函数里,传的仅仅是值,对它本身没有影响。
指针与数组:
如果我们通过函数的参数,把一个数组,传到函数里面去了,那么在这个函数里它接受到的是什么呢?
我们知道,如果传一个普通变量,那么接受到的是一个值,如果传进去一个地址,参数接受到的也是一个值,只不过这个值是地址。
数组是什么?到底我们把一个数组作为值传给一个函数,在函数的参数表里有一个数组变量去接受那个数组,到底接受到了什么?我们知道如果是一个普通变量,那么接受的是值;如果是指针,我们接受到的也是值,是指针的值,它代表了外面的那个变量,那么对于一个数组变量,出现在参数表里面的数组参数,它到底接受到数组变量的一个什么东西呢?或者说接受到的是一个什么值呢?
- 函数参数表中的数组实际上是指针
- sizeof(a)==sizeof(int*)
- 但是可以用数组的运算符[]进行运算
数组参数
- 以下四种函数原型俩俩是等价的:
- int sum(int *ar,int n);
- int sum(int ar[],int n);
- int sum(int*,int);
- int sum(int [],int);
数组变量是特殊的指针
- 数组变量本身表达地址
- int a[10];int *p=a;//无需用&取地址
- 但是数组的单元表达的是变量,需要用&取地址
- a==&a[0] (解释:a的地址就等于a[0]的地址)
- []运算符可以是对数组做,也可以对指针做
- *a相当于a[0]
- *运算符可以对指针做,也可以对数组做
- 数组变量时const的指针,所以不能被赋值
- int a[]相当于int *const a
字符类型
char是一种整数,也是一种特殊的类型:字符。
- 用单引号表示的字符字面量:'a','1'
- ''也是一个字符
- printf和scanf里用%c来输入输出字符
char c=1;和char c='1';不同(看下面的代码)
#include
'1'这是一个字符,我们把这个自变量赋给了char的变量,我们得到它的整数值是49。
这表明在计算机的内部,'1'这个值就是49。每一个字符在计算机内部都有一个值来去表达它,这个值我们可以直接以整数的形式得到的。
字符的输入输出
- 如何输入'1'这个字符给char c?
- scanf("%c",&c);→1
- scanf("%d",&i);c=i;→49
- '1'的ASCII编码是49,所以当c==49时,它代表'1'
#include
上面的结果意思是:作为整数,它的值是49;作为字符,它的值是'1';同一个变量,以%d来输出,它是49;以%c来输出它就是1。
现在把程序改一下:
#include
发现第9行出现了错误,那现在去掉这一行再运行,得到:
上面这些代码都告诉了我们一件事:49和'1'是相等的!一个是整数的形式,一个是字符的形式。
混合输入
下面看看这两行代码有什么不同?
- scanf("%d %c",&i,&c);
- scanf("%d%c",&i,&c);
我们来探寻一下这件事:
#include
也就是说,在%d后面没有空格,我们的读数只读到整数结束为止,后面那个给下面那个;如果是有空格的,不仅读完整数,还会把后面的空格读掉。所以有没有空格是不一样的。
既然字符是一种整数,它可以做整数的运算,比方说
char
得到的是一个B。
如果是
char
这个结果是C
如果是
int
结果是:25。
- 一个字符加一个数字得到ASCII码表中那个数之后的字符
- 两个字符的减,得到它们在表中的距离
大小写转换
- 字母在ASCII表中是顺序排列的
- 大写字母和小写字母是分开排列的,并不在一起
- 'a'-'A'可以得到两段之间的距离,于是
- a+'a'-'A'可以把一个大写字母变成小写字母,而
- a+'A'-'a'可以把一个小写字母变成大写字母
逃逸字符
- 用来表达无法印出来的控制字符或特殊字符,它由一个反斜杠“”开头,后面跟上另一个字符,这两个合起来,组成了一个字符
printf
我们在之前求身高的代码里出现了"5 7",这个东西的意思是说:这个将要",成为一个字符。这是一个字符不是两个字符,这个字符表示的就是那个双引号。之所以这样的原因是:在双引号里你不能直接出现双引号,它会认为其中的两个双引号之间组成了字符串,所以要用"来表达"。这种东西就叫做逃逸字符。
逃逸字符
b做的事情是把下一个输出回到上一个位置上去,即把上一个给覆盖了。但是如果你不输出东西的话,他就起不到任何作用了。比如(看下面的代码):
int
int
所以b通常做的事情是“回去”,但不“删除”。
制表位
- 每行的固定位置
- 一个t使得输出从下一个制表位开始
- 用t才能使得上下两行对齐
Tab的意思就是在行当中的一些固定的位置,而不是固定大小的字符数量。
看下面这个代码:
int
字符数组
char
这不是C语言的字符串,因为不能用字符串的方式做运算——它只是字符数组不是字符串。
那怎么才能定义出C语言的字符串呢?
char
在后面加一个“0”就能把它变为字符串了。
字符串
- 以0(整数0)结尾的一串字符
- 0或'0'是一样的,但是和'0'不同
- 0标志字符串的结束,但它不是字符串的一部分
- 计算字符串长度的时候不包含这个0
- 字符串一数组的形式存在,以数组或指针的形式访问
- 更多的是以指针的形式
- string.h里有很多处理字符串的函数
字符串变量
我们要表达一个字符串,有几种不同的写法——表现形式不同。
- char *star="Hello";
- char word[]="Hello";
- char line[10]="Hello";
字符串常量
- "Hello"
- "Hello"会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0.
字符串
- C语言的字符串是以字符数组的形态存在的
- 不能用运算符对字符串做运算
- 通过数组的方式可以遍历字符串
- 唯一特殊的地方是字符串字面量可以用来初始化字符数组
- 以及标准库提供了一系列字符串函数
字符串变量
字符串常量
char
上面这段代码是一个字符串,既然是字符串,我们就拿他做这样一件事:把“H”换为“B”
我们通常把char*s="Hello World";中的char *s是一个指针,“Hello world”是一个数组。此时s就指向这个数组了。
#include
为了更加明确它们之间的关系及具体表达的含义,我们将代码进行如下改动
#include
我们发现了,s和s2的值是一样的,它们都指向了同一个位置!
原理是这样的:s和s2其实是一个本地变量,它们被放在了一起。s指向了一个字符串,且s2也指向了这里,而这个地方的地址很小(参考了i的地址为)。这个很小的地址位于程序的代码段,并且它是只读的。如果你用s[0]=……相当于对他改写了。编译器为了找个地方给“Hello world”去放,但是因为这个字符串在编译时刻就已经有值了这么一个东西,所以编译器把它放在了一个只能读,不能写的地方,然后让你的指针指向它,并且如果你的程序里面有两个相同的东西,他们会指向同一个地方(如上面列举的s和s2)。也是因为这个原因,它是只读的。
C语言是个早期的语言(上世纪80年代),它在数字的处理上有很大的优势,但是在处理字符上,就稍有逊色。
实际上这个s的类型是在char前面有个const,但是由于历史的原因,编译器接受不带const的写法。
如果你要定义一个能够修改的字符串,需要用数组来定义。
比如,可以写成char s[]="Hello,world!";。它和char*s="Hello World";的区别在于:char*s="Hello World";是说我要指向那个字符串;char s[]="Hello,world!";是说我的这个字符串就在这里。
计算机把char s[]="Hello,world!";放到了本地变量那。
当我们写字符串的时候,到底写成指针的形式还是写成数组的形式?这两个我们选哪一个?
- char *str="Hello";
- char word[]="Hello";
- 数组:这个字符串在这里,作为本地变量空间自动被回收。
- 指针:这个字符串不知道在哪里,它可以用来处理参数,动态分配空间。
意思就是:如果要构造一个字符串,我们用数组;如果要处理一个字符串,我们用指针。
char*是字符串?
- 字符串可以表达为char*的形式
- char*不一定是字符串,char*仅仅表示这有一个指针,指针指向一个字节,或者一串连续的字节,但它不一定是字符串。本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
- 只有它所指的字符数组有结尾的0,才能说它所指的是字符串。
字符串输入输出
相对于新语言来说,C语言处理字符串的能力还是不足的。
如何对字符串赋值?
- char *t="title";
- char *s;
- s=t;
上面所做的事情并没有产生新的字符串,只是让指针指向了t所指的字符串,对s的任何操作就是对t做的。
字符串输入输出
- char string[8];
- scanf("%s",string);
- printf("%s",string);
- scanf读入一个单词(到空格、tab或回车为止)
字符串依然可以像int类型的数字一样进行输入和输出,int类型的为%d;字符串类型的我们用%s来输入输出。
我们看下面这个代码:
#include
我们发现有个"world"没有读到,当然如果在你的程序里再加一个scanf的话,就可以读到这个“world”。
那么在有两个scanf的情况下,中间的那个空格会不会读到?
我们试一下下面这个代码:
#include
我们发现空格并没有被读出。
那么scanf读的是什么呢?
scanf读入一个单词(到空格、tab或回车为止)。
但是这个scanf是不安全的,因为不知道要读入的内容的长度。
那怎么做是安全的呢?
我们可以在%前加一个数字(如下)
#include
上面那个7呢就是告诉scanf你最多只能读7个字符。超过7个字符,就不要了。
常见错误
- char *string //其实这一行仅仅定义了一个指针变量。string可以是将来要指向某个字符串数组的那么一个指针。在这个时候,指针没有被初始化。然后,我们知道这是一个本地变量的话,本地变量是没有默认的初始值的,原来在那个内存里有什么就是什么。(所以如果本来是本地变量,你却想在后面修改它,就容易出错。)
- scanf("%s",string);
- 以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了。
- 由于没有对string初始化为0,所以不一定每次运行都出错
空字符串
- char buffer[100]="";
- 这是一个空的字符串,buffer[0]=='0'
- char buffer[]=="";
- 这个数组的长度只有1!
字符串函数
对于字符串,C语言提供了很多函数,来帮助我们处理字符串。下面是在C标准库里的常用函数,理论上所有C语言的发行版本都应该带有这些函数。这些函数的原型在一个头文件叫做“string.h”。和我们scanf、printf一样,那时候我们要#include <stdio.h>, 当我们用下面函数处理字符串时需要#include <string.h>。
string.h
- strlen
- strcmp
- strcpy
- strcat
- strchr
- strstr
我们先看第一个函数:strlen //所有的函数都是str开头的;len代表length。
- size_t strlen(const char *s); // 作为参数,数组的形式和指针的形式是一样的。数组传进去也是指针。这也全用指针的类型来表达了。其中的const可以让别的函数无法修改你的数组,你应该把它写成const。
- strlen告诉你,s的字符串长度(不包括结尾的0)
我们试一下这个函数。
#include
第二个函数:strcmp //cmp的意思是compare,所以它是用来比较两个字符串的,因为用来做比较,所以依然要用到const,因为在比较的过程中,是不能修改这个字符串的。
- int strcmp(const char *s1,const char *s2);
- 比较两个字符串,返回:
- 0:s1==s2
- 1:s1>s2
- -1:s1<s2
试一下上面的函数:
#include
注意一点:在程序里我们不能写成
#include
我能不能用下面这个方式来表达它们是否相等?
#include
其实上面那样做是不对的,因为数组的比较永远是false。这两个字符串一定不会是相同的地址,当我们去比较这两个数组变量的时候,用==去比较的时候,表达它们是否是相同的地址。
我们现在比较"abc"和"bbc"的大小:
#include
下面比较"abc"和"Abc"
#include
第三个函数:strcpy //cpy代表copy
- char *strcpy(char *restrict dst,const char *restrictsrc);
- 作用:把第二个参数里的src的字符串拷贝到第一个参数所表达的空间里。
- restrict表面src和dst不重叠(C99)
- 返回dst
- 为了能链起代码来
第四个函数:strcat //意思是作连接
- char* strcat(char *restrict s1,const char *restricts2);
- 把s2拷贝到s1的后面,接成一个长的字符串
- 返回s1
- s1必须要具有足够的空间。
安全问题
- strcpy和strcat都可能出现安全问题
- 很容易目的地没有足够的空间去使用它
所以,我们不要去使用它,那使用什么?
安全版本
- char *strncpy(char *restrict dst,const char *restrictsrc,size_t n); //其实它就中间,在参数表多了一个n,这个n的意思是:你能够最多拷过去最多多少个字符。
- char *strncat(char *restrict s1,const char *restrict s2,size_t n); //对于cat来说就是你最多能连上多少个字符。多了就掐掉。因此,它是安全的,不会越界。
- int strncmp(const char *s1,const char *s2,size_t n); //这个n其实不是为了安全,它的作用是说:有点时候我们想做这样一件事,一个字符串的开头是不是abc,我们不想知道后面的,只关心前三个,我们就可以用这种方式,如果n是3,我们就只让他判断前3个。
字符串中找字符
- char *strchr(const char*s,int c); //意思是说我要在s这个字符串中找c第一次出现的位置(从左朝找右),它返回的是指针。
- char *strrchr(const char*s,intc); //意思是说我要在s这个字符串中找c第一次出现的位置(从右朝左找)。
- 返回NULL表示没有找到
- 这两个函数有一个很有意思的套路:我们如何寻找第二个呢?
第五个函数: