printf 指针地址_C语言入门——第七周笔记——指针与字符串

39d3653230f6a21158ee37d6920d52f8.png
“鬼鲛,你,怕死?”
纹路

取地址运算:&运算符取得变量的地址

C语言给出了一个工具——sizeof

  • sizeof是一个运算符,给出某个类型或变量在内存中所占据的字节数
  • sizeof(int)
  • sizeof(i)
#include 

8c4b3e610672556802b7ebc0c86340d8.png

int 占了4个字节,对应32个bit;double 占了8个字节,对应64个bit(一个字节=8bit)

运算符&

  • scanf("%d",&i);里的&是一个运算符。和加减乘除一样。
  • 他能够获得变量的地址,它的操作数必须是变量
  • 地址的大小取决于架构,取决于编译器
  • int i;printf("%p",&i);

&不能取的地址

  • &不能对没有地址的东西取地址

比如
&(a+b)
&(a++)
&(++a)
它必须对一个明确的变量取地址。

试试这些&

  • 变量的地址
  • 相邻的变量的地址
  • &的结果的sizeof
  • 数组的地址
  • 数组单元的地址
  • 相邻的数组单元的地址
#include 

9e625459202030c5b99e5c95de09bb1b.png

4c和48的差别在哪呢?

i是先定义的变量,p是后定义的变量。i在更高的地方,48在更低的地方。他们分配一种叫做“堆栈”的地方。在“堆栈”里,我们分配内存是自顶向下来分配的。所以,我们先写的那个变量地址更高,后写的那个变量地址更低,但是他们是紧挨着的,他们的差距为4。这个4就是sizeof(int)。

#include 

f3ede9ace6beb3d3aee106fbf52699b3.png

前三排数字一样说明了:&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 

32ecdbd51e56e5a5a78c2cb4cc20d2a3.png

这就相当于在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 

c2328b24c4f8acc80984c8c20276e243.png

我们下面做一件更邪恶的事:

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 

d01635589dc5a2ba2d9fae5f105ea6b1.png

'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 

c9f78b36988c5412593ca958cf0c65ca.png

上面的结果意思是:作为整数,它的值是49;作为字符,它的值是'1';同一个变量,以%d来输出,它是49;以%c来输出它就是1。

现在把程序改一下:

#include 

4acee977a5832e6fbb8b8ec9a518aaff.png

发现第9行出现了错误,那现在去掉这一行再运行,得到:

5fa0560b60bd9dc6a7b23748316968f2.png

上面这些代码都告诉了我们一件事:49和'1'是相等的!一个是整数的形式,一个是字符的形式。

混合输入

下面看看这两行代码有什么不同?

  • scanf("%d %c",&i,&c);
  • scanf("%d%c",&i,&c);

我们来探寻一下这件事:

#include 

d0467cba250e409a2a4fda222292a0b1.png
前3行为%d %c中间有空格的情况,后三行为中间无空格的情况

就是说,在%d后面没有空格,我们的读数只读到整数结束为止,后面那个给下面那个;如果是有空格的,不仅读完整数,还会把后面的空格读掉。所以有没有空格是不一样的。

既然字符是一种整数,它可以做整数的运算,比方说

char 

得到的是一个B。

如果是

char 

这个结果是C

如果是

int 

结果是:25。

  • 一个字符加一个数字得到ASCII码表中那个数之后的字符
  • 两个字符的减,得到它们在表中的距离

大小写转换

  • 字母在ASCII表中是顺序排列的
  • 大写字母和小写字母是分开排列的,并不在一起
  • 'a'-'A'可以得到两段之间的距离,于是
  • a+'a'-'A'可以把一个大写字母变成小写字母,而
  • a+'A'-'a'可以把一个小写字母变成大写字母

逃逸字符

  • 用来表达无法印出来的控制字符或特殊字符,它由一个反斜杠“”开头,后面跟上另一个字符,这两个合起来,组成了一个字符
printf

我们在之前求身高的代码里出现了"5 7",这个东西的意思是说:这个将要",成为一个字符。这是一个字符不是两个字符,这个字符表示的就是那个双引号。之所以这样的原因是:在双引号里你不能直接出现双引号,它会认为其中的两个双引号之间组成了字符串,所以要用"来表达"。这种东西就叫做逃逸字符

逃逸字符

0348f8e01948c635190e652eea5b466c.png

b做的事情是把下一个输出回到上一个位置上去,即把上一个给覆盖了。但是如果你不输出东西的话,他就起不到任何作用了。比如(看下面的代码):

int 

3c373488b775d5b115a96029d22b1ab6.png
这是在不输出东西的情况下
int 

355cfd4c829cbf0d482dc4e0ed71de5e.png
此时A把3给覆盖了

所以b通常做的事情是“回去”,但不“删除”。

制表位

f37dde2d030c9e1bf9ec418bc1008b6d.png
  • 每行的固定位置
  • 一个t使得输出从下一个制表位开始
  • 用t才能使得上下两行对齐

Tab的意思就是在行当中的一些固定的位置,而不是固定大小的字符数量。
看下面这个代码:

int 

9cd4d04444d7879161d34cf4a47839ff.png

字符数组

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 

96aec79c3ecac1e2f6bc54a088dfec1c.png
运行代码出现了这个

为了更加明确它们之间的关系及具体表达的含义,我们将代码进行如下改动

#include 

433c1b22af7f6be00fe786939db98f6d.png

我们发现了,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 

6d63b3ab025ac97b7eaba1809549d49d.png

我们发现有个"world"没有读到,当然如果在你的程序里再加一个scanf的话,就可以读到这个“world”。

那么在有两个scanf的情况下,中间的那个空格会不会读到?

我们试一下下面这个代码:

#include 

336d458edae50825c16f357cd575bc34.png

我们发现空格并没有被读出。

那么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 

c785661e2ce4557fcc8d0c75f750421a.png
strlen不计算0

第二个函数:strcmp //cmp的意思是compare,所以它是用来比较两个字符串的,因为用来做比较,所以依然要用到const,因为在比较的过程中,是不能修改这个字符串的。

  • int strcmp(const char *s1,const char *s2);
  • 比较两个字符串,返回:
  • 0:s1==s2
  • 1:s1>s2
  • -1:s1<s2

试一下上面的函数:

#include 

d9e9aae602ccc5cee40e9fe9b4b44593.png
0表面这两个字符串是相等的;

注意一点:在程序里我们不能写成

#include 

我能不能用下面这个方式来表达它们是否相等?

#include 

4d8942b896f660dd3aa3c5dfbe4508da.png

其实上面那样做是不对的,因为数组的比较永远是false。这两个字符串一定不会是相同的地址,当我们去比较这两个数组变量的时候,用==去比较的时候,表达它们是否是相同的地址。

我们现在比较"abc"和"bbc"的大小:

#include 

ca0a7f181f464316b21b3b1f99f3b212.png
说明了s1比s2小,因a在字母表的前面

下面比较"abc"和"Abc"

#include 

5771d06a7a83c429a84a0dc063e6bb19.png

第三个函数: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表示没有找到
  • 这两个函数有一个很有意思的套路:我们如何寻找第二个呢?

第五个函数:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值