这个大长篇相当于是自己对于c语言学习的一个总结,会持续更新完善。
后续会在寒假整理一些经典的例题附带题解,当然希望我学到的东西、总结的经验,能够给后来者提供一个更好的学习途径,从入门到精通而不再是放弃。
也欢迎读者提出宝贵的意见和建议,也欢迎你跟我一起成长。
内存
编址:内存以字节为单位线性连续编址,即按照0x0000,0x0001,0x0002,…的方式;从低地址端开始向高地址端为每一个内存字节进行顺序连续编号
基本的单位代换
1byte = 8 bit 即一字节等于八位,1位就相当于一个二进制位‘0’或‘1’
字节(Byte)=8位(bit)
1KB( Kilobyte,千字节)=1024B
1MB( Megabyte,兆字节)=1024KB
1GB( Gigabyte,吉字节,千兆)=1024MB
1TB( Trillionbyte,万亿字节,太字节)=1024GB
基本数据类型
1、整型
short:-32768~32767[-2^15]~[2^15 - 1]
存储长度为2 B
unsigned short存储长度为2 B,[无符号短整型]
int: -2147483648~2147483647[-2^31~2^31-1]
存储长度为4 B(一般电脑)
unsigned int: 存储长度为4 B,[无符号整型]
long:[-2^63~2^63-1]
存储长度为8 B
unsigned long存储长度为8 B,[无符号长整型]
2、字符型
char: -128~127
存储长度为一个字节
3、浮点型
float和double是由1位符号位+指数位+尾数位构成的,具体情况比较复杂,新手可暂时略过这一部分。
float 单精度4 B
double 双精度8 B
划重点!浮点数的存储是有误差的,这跟浮点型的存储机理有关,直接运算和比较会造成误差。
比较方法:将两数值之差同一个预定的小正数比较。
进制转换
- 十进制整数和二进制整数:除2取余法/按权相加
- 二进制转八进制:3位分组法。从整数部分的最低位起,每3位分成1组,高位部分不足3位则通过加前导0的方式补足3位,然后把每3位二进制数用对应的八进制数来表示即可。
- 八进制转二进制:八进制整数转换为二进制整数只需要将每一位八进制数用对应的二进制数表示即可。
数据的二进制表示
原码和补码
原码:对于一个二进制数X,如果规定用最高位为符号位,其余各位为该数的绝对值。并且规定符号位之值为0表示正,符号位之值为1表示负,则采用这种方式形成的二进制编码称为称为该二进制数X的原码。
补码:补码的定义是正数的补码等于正数的原码,负数的补码为其原码除符号位不动,其余各位变反再加1所得。
反码:对正数而言,其反码与原码、补码的表示相同;对负数而言,反码符号位的定义与原码、补码相同,但需要将对应原码的数值位按位变反。
正数的原码、补码、反码都相同
常量与变量
常量
1、整型常量
- 通过前缀字符区分不同的进制表示方式
十进制:无前缀
八进制:前缀为0
十六进制:前缀为0x或0X
- 整型常量可以带有后缀用以指定其类型
u(U)表示unsigned
l(L)表示long
ll(LL)表示long long
ul(UL)表示unsigned long
2、浮点型常量
- 带小数点的十进制数形式(23.3 , 12. , .231)以小数点开头和结尾都可以
- 指数形式(科学计数法)(23e-2 = 23x10-2 , .15e5 = 0.15x102)
3、字符常量
- 单引号包含的字符 ‘C’
- 只能包含一个字符
双引号包含的是字符串常量
区别 ‘a’ 和 “a”:
‘a’ : 字符常量,占1 B内存空间
“a” : 字符串常量,占2 B内存空间 (字符串常量以’\0’结尾,多存储了一个’\0’)
4、转义序列
常用的转义序列有:\n(换行),\t(制表),\0(空字符),\\(反斜杠),
用#define定义符号常量:
#define 标识符 常量 //注意不要加分号
标识符一般用大写,以区分变量,其后的书写即可用该标识符代表这一常量
用const定义符号常量:
const 类型名 标识符=常量;
const double pi = 3.1415926;
5、枚举常量的定义
关键字enum
enum 枚举名 {枚举常量};
enum week { SUN, MON, TUE, WED=3, THU, FRI, SAT ,};//结尾的逗号可有可无,枚举名也可以省略
在未指定值的缺省情况下,第一个枚举常量的值为0,以后的值依次递增1
可以指定一个或多个枚举常量的值,未指定值的枚举常量的值比前面的值大1
枚举变量的声明:enum week c1;
一个枚举变量的值是int型整数,但值域仅限于列举出来的范围
6、布尔类型
头文件stdbool.h定义宏名bool为_Bool, false和true分别为0和1
如何将一个较长的字符串写成多行?
这里就涉及到行连接问题:
1、在前一行的末尾输入续行符(\) 再换行
"hello,\
are you OK?" 换行后应紧靠行首
2、将字符串分段,分段后的每个字符串用双引号括起来。
“hello,”
“are you OK?”
变量
定义与命名
变量代表内存中具有特定属性的一个存储单元,它用来存放数据,这就是变量的值,在程序运行期间,这些值是可以改变的。
命名规则:以一个字母(a~z, A~Z)或下划线( _ )开头,后跟字母、下划线或数字(0~9),不使用c语言的保留关键字(即在c语言里有特定意义的名称)
以字母和下划线以外的符合开头、定义为保留字都是非法的
驼峰命名法
混合使用大小写字母来构成变量和函数的名字。
第一个单词以小写字母开始;从第二个单词开始以后的每个单词的首字母都采用大写字母例如:myFirstName、myLastName
程序员们为了自己的代码能更容易的在同行之间交流,所以多采取统一的可读性比较好的命名方式。
类型转换
C语言允许双精度、单精度、整型及字符数据之间混合运算
10 + ‘1’ + 6.5
但有一个规则: 先转换成同一类型(int 或 unsigned int)再计算。
任何表达式中的char、unsigned char、short、unsigned short都要先转换成int或unsigned int,如果原始类型的所有值可以用int表示,则转换成int,否则转换成unsigned int,把这称为整数提升
赋值转换:右操作数的值被转换为左操作数的类型
short s = 5;
double d = 2.9;
s = d; //s = 2(d转换成short后值为2)
d = s; //d = 2.0
强制类型转换:
(类型名)操作数
(int)i //将i转换为int类型
基本的输入和输出
1.getchar
函数的调用形式为:
getchar()
原型:int getchar(void);
调用时,圆括号中不能带参数,但必须保留圆括号,函数执行时从输入流中读取一个字符,并将所读取的字符(类型为unsigned char)转换为int类型后作为函数的返回值。
2. putchar
函数的调用形式:
putchar(c)
原型:int putchar(int c);
c为实际参数,它可为char、short与int类型的表达式,其值是要输出字符的字符码。函数正确执行时返回该字符码,否则返回EOF。
putchar(i =’ ’); //输出一个空格
putchar(i = 32); //输出一个空格
3.puts
函数的调用形式为:
puts(s)
原型: int puts(const char *s);
const char *s中的const表明字符指针s的值不会被该函数修改。
其中,s为实际参数,可以是字符串常量、字符数组名,或指向某字符串的字符指针变量.puts函数从s所指定的地址读取字符串输出到标准输出设备,并在串尾输出一个换行符’\n’。
字符串在内存缓冲区存储时串尾以空字符**’\0’作为结束标志,puts取字符串时从s指定的内存区依次取字符直至取到空字符为止**。puts函数正确执行时返回一个非负整数值,如果出错,则返回EOF。
4.gets
函数的调用形式为:
gets(s)
原型:char *gets(char *s);
gets从输入流中读取一行字符存放s指定的内存缓冲区,结尾的换行字符’\n’被空字符’\0’所替换,以作为字符串的结束标志。
函数正确执行时返回该内存缓冲区的首地址,即s的值;如果遇到文件尾或出错,则返回空指针NULL。
gets() 函数不进行边界检查,从而此函数对缓冲区溢出攻击极度脆弱。无法安全使用它(除非程序运行的环境限定能出现在 stdin 上的内容
5.printf
函数的调用形式:
printf(格式字符串, 数据项1, …, 数据项n)
*原型:int printf(const char format, …);
第一个形式参数format是一个字符串,称为格式字符串,用来指定输出数据的个数和输出格式。其余参数是要被输出的数据,参数的个数和数据类型应与格式字符串中转换说明的个数和转换字符(参见下表)一致。
printf函数的返回值是函数调用时实际输出到标准输出设备的字符个数。
转换说明
以%字符开始,以转换字符结尾:
转换字符 | 参数类型 | 输出格式 |
---|---|---|
d,i | int | 十进制整数 |
o | int | 八进制整数(不带前缀0) |
x,X | int | 十六进制整数(不带前缀0x或0X) |
ld,hd | long int,short int | 长整型/短整型 |
u | int | 无符号整型 |
c | char | 单个字符 |
s | **char *** | 字符串(必须以\0结束或在域宽说明中给出长度限制) |
p | void * | 指针值(地址) |
f | float/double | 小数形式的浮点数(小数部分位数由精度确定,缺省为6位) |
e, E | double | 标准指数形式的浮点数(尾数部分位数由精度确定) |
g, G | double | 在不输出无效零的前提下,按输出域宽度较小的原则从%f、%e中自动选择 |
Lf | long double | 长双精度浮点型 |
% | %%表示输出一个% |
域宽说明
在%和转换字符之间可以有域宽说明字符(%m.n+转换字符),用来指出输出数据的对齐方式、输出数据域的宽度、小数部分的位数等要求。
域宽说明字符 | 意义 |
---|---|
- | 表示左对齐输出,如省略表示右对齐输出。 |
+ | 带符号输出,输出正数时前面要加+号 |
空格 | 输出的第一个字符不是+或-时以空格为前缀 |
# | 对于o和x格式输出前缀0和0x,对于g格式不删除尾部零 |
m(正整数) | 输出数据的最小域宽(实际宽度小于m则左边补空格或0) |
0 | 在输出的域宽范围内用前导0补齐空位 |
. | 分隔域宽和精度,小数点前域宽可以省略 |
n(正整数) | 输出数据的精度。对于e、f格式为保留的小数位数,对g格式为保留的有效数字位数,对整数为至少应输出的数字的位数(用前导0补足),对字符串为至多输出的字符数目 |
* | 代表一个整数,其值由对应参数决定,可用于代替m或n表示可变宽域或精度 |
//可变域宽示范
int max=6;
printf(“%*c”, max,’ ’);
//输出6个空格(*代表的域宽由对应参数max决定)
6.scanf
函数的调用形式
scanf(格式字符串, 输入参数1, 输入参数2, …,输入参数n);
原型: int scanf(const char *format, …);
输入参数1至输入参数n可为基本类型或指针类型变量的地址(即指针),可以是字符数组名或指向字符数组首元素的指针变量。scanf函数正确执行时,返回值为被转换并赋值的数据的个数,遇到文件尾或出错时返回EOF。
scanf函数的格式字符串与printf函数相似
在实际使用中,scanf函数的格式字符串一般只需包含转换说明。
对于scanf函数格式字符串中除空格和制表符外的其他普通字符,在输入流中相应位置必须输入相同的字符,否则scanf函数读不到正确的数据.如果在scanf函数的格式字符串中加入了除空格和制表符以外的普通字符,不仅给数据输入带来麻烦,而且容易出错
char s[20];
int i;
scanf("%d %s",&i,s); //非指针变量的输入一定要取地址
转换说明 | 使用类型 |
---|---|
h | 短整数(short) |
l | 长整数(long int),双精度浮点数(double) |
L | 长双精度浮点数(long double) |
* | 与该转换说明对应的输入被跳过(虚读) |
基本流程控制语句
1.判断语句
- if
嵌套if语句中else的配对规则: else与其前面最靠近的还未配对的if配对,即内层优先配对原则
if ( n > 0 )
if ( a > b ) z = a;
else z = b;
- switch
switch语句的一般形式为:
switch(表达式) {
case 常量表达式1:语句序列1; break;
case 常量表达式2:语句序列2; break;
…
case 常量表达式n:语句序列n; break;
default: 语句序列n+1; break;
}
2.循环语句
- while
-
while(表达式) {执行语句}
-
do {执行语句} while(表达式)
执行语句至少执行一次
- for
for(e1; e2; e3){执行语句}
e2为满足循环的条件
//基本样例
int sum = 0;
for(int i = 0; i < n; i++){sum = sum + i;
}
三个表达式可以全部或部分缺省,但无论缺省e1,e2还是e3,它们之间的分号不能省
3.break语句
break语句有以下两种用途:
**(1)**用于switch语句中,从中途退出switch语句;
(2)用于循环语句中,从循环体内直接退出当前循环。
挖坑:如何退出多重循环?
4.continue语句
continue语句只能出现在循环语句中,用于终止循环体的本次执行(并非退出循环语句);即在循环体的本次执行中,跳过从continue语句之后直到循环体结束的所有语句,控制转移到循环体的末尾。
int sum = 0;
for(int i = 0; i < n; i++){if(i%2==0) continue; //满足条件时跳过本轮循环sum=sum + i;
}
5.return语句
return语句有下面两种形式:
**(1)**不带表达式的return语句:
return;
return语句的功能是从被调用函数返回到调用函数。不带表达式的return语句只能返回控制、不能返回值,因此只能用于从无返回值的函数中返回。
**(2)**带表达式的return语句:
return 表达式;
在返回控制的同时,将表达式的值返回到调用处,函数调用表达式的值就是这个返回值。
函数与程序结构
结构化编程是一种解决问题的策略
(1) 程序中的控制流应该尽可能简单。
(2) 应该自顶向下地设计程序结构。
“分而治之”逐步细化,把一个问题按功能分解为若干子问题,如果子问题还较复杂,可将其继续分解,直到分解成为容易求解的子问题为止。分解而来的每个子问题被称为模块,C中提供的函数机制完成每个模块的编程任务,即用函数编写由分解而来的子问题的代码。
自定义函数
类型名 函数名(参数列表)
{
声明部分
语句部分
}
**类型名**说明函数返回值(即出口参数)的数据类型(简称为函数的类型或函数值的类型)
(1) return; //void函数
(2) return 表达式; //非void函数
void函数可以不包含return语句。如果没有return语句,当执行到函数结束的右花括号时,控制返回到调用处。
函数返回的值,程序可以使用它,也可以不使用它
**参数列表说明函数入口参数的名称、类型和个数,每一个形参都必须有数据类型和名字
double power(int x, int n) { … } /* 正确的函数参数定义 */
double power(int x, n) { … } /* 错误,n必须指定类型 */
int GetNum (void) { … } /* 参数表为空的函数 */
良好的编程风格是:在每个函数的顶端用/*...*/格式增加函数头部注释
为函数命名时,要选择有意义的名称,以增加程序的可读性,还可避免过多地使用注释。通常用“动词”或者“动词+名词”(动宾词组)形式。
函数的声明
在调用函数之前,必须给出调用函数的函数定义
类型名 函数名(参数类型表);
void GuessNum(int) ;
等价于
void GuessNum(int x ) ;
无参函数的函数原型参数表必须指定为void
函数原型告诉编译器所定义的函数的性质,编译器用函数原型校验函数调用,从而避免错误的函数调用导致的致命运行错误或难以检测的非致命逻辑错误。
引入标准头文件的主要原因是它含有函数原型。
实参的求值顺序
a=1;
power(a, a++)
从左至右:power(1,1)
从右至左:power(2,1) (多数)
为了保证程序清晰、可移植,应避免使用会引起副作用的实参表达式
参数的值传递
参数的传递方式是值传递,实参的值单向传递给相应的形参。在函数内改变了形参的值但并不能改变主函数中变量的值(即局部变量)。
在不覆盖全局变量的前提下,函数是可以改变全局变量的值的
那怎么改变函数外部的局部变量的值呢?
这时候我们就需要传入变量的地址(指向该变量的指针),对地址(指针)进行解引用。
标准库函数scanf就是一个引用调用的例子
(想要进一步了解可以跳到指针的章节哦)
作用域和可见性
作用域
指标识符(变量或函数)的有效范围,有局部和全局两种作用域。
局部作用域表示只能在一定的范围内起作用,只能被一个程序块访问。
块范围:其作用域开始于左大花括号{ 结束于右大花括号}
全局作用域表示可以在整个程序的所有范围内起作用,可由程序中的部分或所有函数共享
局部变量:在函数内部定义的变量,作用域是定义该变量的程序块,不同函数可同名,同一函数内不同程序块可同名。形式参数是局部变量。
for(int i; i < n; i++){} //i为for语句块的局部变量
int sum(int n,int m); //n,m为sum函数块的局部变量
**局部变量默认的存储类型是auto**
外部变量:在函数外部定义的变量,其作用域从其定义处开始一直到其所在文件的末尾,可由程序中的部分或所有函数共享。
int i = 0; //i为外部变量
int sum(int n,int i); //i会覆盖外部变量,但函数不改变外部变量
int main(void){}
extern声明:在函数中使用外部变量,一般要对该变量进行引用性声明,说明它的类型。但在一个外部变量定义之后的函数内使用可不再加以声明
extern int sp;int sp;
外部变量的定义必须在所有的函数之外,且只能定义一次,目的是为之分配存储单元。外部变量的初始化只能出现在其定义中。
外部变量的引用性声明既可以出现在函数内,也可以出现在函数外,而且可以出现多次,仅用于通报变量的类型,并不分配存储单元.
static:
(1) 用于定义局部变量,称为静态局部变量。
静态局部变量,只作用于定义它的块。当退出块时,它的值能保存下来,以便再次进入块时使用。只执行一次赋初值操作,而自动变量每次进入时都要执行赋初值操作
使用静态局部变量是为了多次调用同一函数时使变量能保持上次调用结束时的结果。即静态局部变量的值有记忆性。
static int i = 1;
(2) 用于定义外部变量,称为静态外部变量。
可见性
作用域的嵌套:程序块可以多重嵌套,每个块都可以定义自己的变量名。外层块的变量名在内层块中是有效的。但是,如果一个变量名a同时出现在外层块和内层块中,外层a的作用域包含了内层a的作用域,这称为作用域的嵌套。
当内层的变量和外层的变量同名时,在内层里,外层的变量暂时失去了可见性,是不可见的。
同样地,全局变量和局部变量也可以同名。在局部变量的作用域内,全局变量不可见。
- 外部函数
函数一般是全局的,作用域属于文件范围,对程序的任何部分都是可见的,其默认存储类型是extern
函数定义时一般省略extern,在其他需要调用它的文件中,一般用extern声明
extern int GetNum (void);
编译预处理
对源程序进行编译之前所作的工作,它由预处理程序负责完成。
预处理指令:以**“#”**号开始的指令。
文件包含#include
(1) #include <文件名>
在指定的标准目录下寻找被包含文件
(2) #include "文件名"
首先在用户当前目录中寻找被包含文件,
若找不到,再在指定的标准目录下寻找
< >内“ ”也可以包含文件地址,此时两者性质相同,都是到指定文件地址处寻找
宏定义#define
#define 标识符 字符串
宏名:被定义的标识符。
宏代换(宏展开):用字符串去取代宏名,用实参去替换形参
`**定义带参数的宏时,为了保证计算次序的正确性,表达式中的每个参数用括号括起来,整个表达式也用括号括起来。
#define muti(a,b) ((a)*(b))
int main(){int i = 2,c = 3;int k = muti(i+c,c);
}
//宏展开后为 k = ((i+c)*(c))
条件编译
用于在预处理中进行条件控制,根据所求条件的值有选择地包含不同的程序部分,因而产生不同的目标代码。 这对于程序的移植和调试是很有用的。
1 #if、#ifdef指令
#if : 编译预处理中的条件命令,相当于C语法中的if语句
#define R
int main(){....
#ifdef R //判断R是否被定义
r=3.14159*c*c;
printf(“%f\n”,r);
#else //此时这一段代码不参与编译
s=c*c; //此时这一段代码不参与编译
printf("%f\n",s);//此时这一段代码不参与编译
#endif
return 0;
}
2 defined运算符
#if defined(MACRO_1) && defined(MACRO_2)程序段A
#endif
3#ifndef指令
#ifndef指令检查这个标识符是否未被定义,如果已被定义,则说明该头文件已经被包含了,就不要再次包含该头文件,#ifndef就帮助编译器跳过直到#endif的所有文本;反之,则定义这个标识符
//防止重复包含头文件的宏
#ifndef _NAME_H
#define _NAME_H /* 定义头文件的标识符 */
…… /* 头文件的内容 */
#endif
//NAME是头文件的名字。比如头文件为myFile.h,则其标识符可为_MYFILE_H
assert断言
在头文件assert.h中,用来测试表达式的值是否符合要求,其形式如下:
assert(condition)
如果condition值非0,程序继续执行下一个语句。
如果condition值0,就输出错误信息,并通过调用实用库中的函数abort终止程序的执行。
ps: 在头文件assert.h的assert宏定义中,如果定义了符号常量NDEBUG,其后的assert将被忽略。因此,如果不再需要assert,那么可把代码行:
#define NDEBUG
插入到程序中,而无需手工删除assert。
数组
数组特点
其所有元素:数目固定
其所有元素:类型相同
其所有元素:顺序存放 (在内存中也是连续存放的)
从内存存储的角度看数组代表的是内存中一片连续存储区域,该区域占用的字节数等于元素个数与每个元素占用的字节数的乘积。
定义数组
类型说明符 数组名[常量表达式]={初值表};
数组名:是一个标识符,是一个地址常量,用以表示数组中打头元素的地址
有初始化值时,长度说明可缺省
int y[]={1,2,3,4,5,6,7,8};
初始化值的个数可以小于说明长度,但只能缺省最后连续元素的初值
int z[10]={0,1,2,3,4}; /*前5个下标变量赋值*/
int u[9]={ , 1, , ,2}; 错误:缺省u[0], u[2]不是最后连续元素
访问数组:数组名[下标表达式]
一维数组作为函数参数
传送的是数组的地址或传送数组元素的地址
还需传送数组元素的个数
void bubble_sort(int a[],int n) //长度必须是外部传入
{ //对a[n]表示的n个整数进行排序处理
}
字符数组
以字符为元素的数组称为字符数组,通过字符数组可以构造字符串
用一个字符数组来存放字符序列,并且在末尾加一个空字符ˊ\0ˊ来构造字符串
char a[10]={"HUST"};
//等价于
char a[10]={'H','U','S','T','\0'};
printf("%s",a);
//输出HUST
‘H’ | ‘U’ | ‘S’ | ‘T’ | ‘\0’ |
---|---|---|---|---|
a[0] | a[1] | a[2] | a[3] | a[4] |
字符数组的最小长度应该等于该字符串的长度加1
通过下标来访问字符数组中的具体字符元素
- 关于字符串的一些处理函数在<string.h>中
多维数组
类型说明 数组名 [常量表达式1] [常量表达式2]…[常量表达式n]={初值表};
二维数组可以描述数学中的矩阵或行列式
声明时不能缺省二维的长度说明
- 省略第1维的方式
char devices[ ] [12]={“hard”,“disk”,“keyboard”};
-
引用单个字符元素
weekend[i] [j] =m;
引用字符串
weekend[i]表示weekend数组中第i行字符串的首地址
输出用:printf("%s",weekend[i]);
三维数组可以描述空间中的点集
指针
概念引入
数据(变量、常量)(根据类型)占有一定数目的连续存储单元,数据的连续存储单元首地址称为数据的地址。
变量的地址称为指针,存放地址数据的变量称为指针变量
指针变量也是一种变量,也要占用一定的内存单元。指针变量的特殊之处在于它存放的是另一个变量所占存储单元的起始地址
创建一个指针变量
类型名 *指针名 = 变量地址;
int c = 10;
int *pc = &c;
//在声明的时候对其进行初始化,初始化后的指针将指向以初值为地址的变量。
取地址运算符-单目 &
单目 & 在C语言中表示取地址运算,它只有唯一一个右操作数。
*如果操作数的类型是T,则表达式(&操作数)的类型是T 。
指针与常量
const int x=10; //定义一个常整型数据类型,其值不能被修改
const int *p = &x; //定义一个指向整型常量的指针,所指对象不能修改,但指针本身的内容可以被修改
const int *const px=p;//常量指针px不能修改,指向的变量x也不能被修改
空指针
void指针是一种准通用型指针。
*void vp;
它可以被用于存储任何类型的指针值,也可以将其存储的指针值通过显示的强制类型转换赋给与指针值同类型的指针变量
int x=1,*px=&X;
char ch='a',*pch=ch,*pc;
void *vp;
vp=px; //合法
vp=pch; //合法
pc=(char *)vp; //合法
*vp='b'; //非法
不能对void指针执行间访操作,即对void指针施行“*”操作属于非法操作
解读一些复杂的指针变量
在csdn上曾经看到过非常好理解的一篇文章的解读,分享给大家
1.指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。2.指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?
1.int (*p1)[3]; //p1是指向有3个元素的整型数组的指针。2.double *p2[5]; //p2是有5个元素的double类型的指针数组,每个元素都可以指向一个double类型的变量。
3.char (*fp)(int,int); //fp是一个函数指针,所指函数有2个整型形参,并且返回值的类型为char类型。 4.int *pf(float a); //pf是一个有1个float类型形参的float类型的指针函数,返回值为int *类型的指针5.int (*fp_ary[2])(char *,int *);
//fp_ary是一个有2个元素的函数指针数组.即:该函数指针数组中的每一个元素都是一个函数指针。每个函数指针所指的函数都有2个形参并且所指函数返回值的类型为int类型。
指针的使用
建立指针变量与被指变量间的指向关系后,通过指针变量来间接访问和操作指针所指的变量
间访运算符单目*
1.使用单目*这个间访运算符能实现通过指针对指针所指变量的快速访问 。
2.间访的含义是间接访问,指通过变量的地址来间接访问变量,它与汇编语言中的间接寻址意思相近。
*操作数
操作数也可以是表达式,但其值必须是地址值。
指针的运算
- 指针的移动:对指针实施+、-、*、/,指针值变化的最小单位是sizeof(T)T是类型名
p+n引起物理地址的增量n×sizeof(T)
- ++和–的指针算术运算
1.前缀式++p或–p
++p使指针p先自增指向其后的一个元素,–p使指针p自减指向其前面的一个元素,然后再进行其他操作。
2.后缀式p++和p–
此时使用的是指针的原值,使用结束并且到达序列点时,p++使指针自增指向其后的一个元素,p–使指针自减指向其前面的一个元素。
- 指针变量相减
指向同一数组中元素的指针变量之间可以进行减法运算。
设指针变量p、q是指向同一数组元素,且p>q。
则p-q为p和q之间相隔元素的个数。
应用:利用指针变量相减求字符串的长度。
- 指针的赋值运算
对指针的赋值一般分为两种情况,类型相同时和类型不同时
1.类型相同:可以直接使用赋值操作符进行赋值操作,常量0(NULL)可以赋给任何类型的指针变量
int a[3]={1,2,3},*p=a,*q=p;
2.类型不同:必须使用类型强制
int x=300;
char *p=(char *)&x; /*类型强制,x被视为4字节字符数组被p所指*/
利用指针的赋值运算及类型强制可以实现一些特殊操作。例如字节的截取拆分,进行一些二进制位的操作。
- 指针的关系运算
指针的关系运算指对指针施行诸如:<, <= , > ,>= , ==,以及!=这样一些比较操作。
px<&a[0]+5
实际应用中,指针的关系运算多用于循环控制条件中控制循环的终止。
注:不同类型指针之间的关系运算视为非法
指针作为函数的参数
指针作为函数的参数是指以某个对象的地址值作为函数的参数。
这里我们将指针的传递与一般的参数传递(值传递)做一个对比:
-
值传递:
在函数调用过程中,参数传递是将实参之值复制传递给形参单元,因此形参是实参的副本。如果不使用指针,在值传递过程中,被调用函数中对形参的修改无法影响调用函数中实参变量的值。
void swap(int a,int b); //声明交换函数原型
int main(){
int x=3,y=5;
swap(x,y); //以x,y之值为实参调用交换函数
return 0;
}
//在被调用函数swap中,形参a和b的值被交换,但是main函数中的实参x和y的值却没有被交换。
其原因就是实参x和y仅仅将其值传递给形参u和v;swap函数中对u和v的操作对实参变量x和y没有任何影响。
仅仅通过值传递,不能达到在被调用函数中修改和操作调用函数中某些变量的目的。
为了让被调用函数能够修改和操作调用函数中的某些变量,可以用指向这些变量的指针或这些变量的地址作为函数的实参
void swap(int *,int *)
或者
void swap(int *p1,int *p2)
{int tmp;tmp = *p1;*p1 = *p2;*p2 = tmp;
}
调用时
swap(&x,&y); /*以x,y之址为实参*/
指针和数组
指针与数组之间存在着密切的联系,如果指针能够指向这块连续的内存区域,通过指针的运算就可以实现对这块内存区域中指定元素的快速访问
-
数组元素既可以用下标表示,也可以用指针表示
*a[0] == a ;
*a[i] == (a+i);
-
运算效率方面
下标操作符[]是系统的预定义函数
下标操作实际涉及对系统预定义函数的调用
因此,一般来说指针运算比数组下标运算的速度要快
-
应用:
为了用指针变量表示数组中的元素,应该先声明一个数组,再声明一个与数组名类型相同的指针变量,然后通过初始化或赋值操作使指针变量指向数组中的元素。然后就可以通过指针变量快速访问数组中的其他元素
*1.int a[5],p=a;
*2.char s[20],pc=s;
运算操作 | 操作意义 |
---|---|
*(pc++) | 取得pc后一个元素 |
*pc++ | 取得pc所指元素,pc自增一位 |
(*pc)++ | pc所指元素自增1 |
++*pc | pc所指元素自增1 |
*++pc | pc加1后所指的元素 |
&pc | 字符指针变量pc的地址,类型为char ** |
s+i | 结果为数组s中第i个元素的地址 |
(*s)++ | 结果为元素s[0]的值,然后s[0]加1。 |
s++ | 对数组名的自增自减是非法的 |
&s | 非法,数组名s是地址常量,不能取地址 |
s=pc | 非法,数组名s是地址常量,不能进行赋值操作 |
注:当一维数组用作函数参数时
int func(int a[ ]){...};
//等价于
int func(int *p){...};
//此时a是可以作为指针用,做a++等一些指针能做的操作的
匿名数组
表示为复合文字:
(类型名){初值表}
int *p = (int []){2, 4};
该语句声明了指针p,并且使指针p指向有两个元素的整型数组中的开头元素。
声明语句中的(int []){2, 4}就是一个复合文字,它实际表示一个匿名数组。
用指向数组基本元素的指针表示多维数组
对于二维数组,设有声明:
int u[M][N];
int *p=&u[0][0]; //p=*u或p=u[0]
//指针p间访其u[i][j]元的形式为: *(p+i*N+j)
对于三维整型数组v[L] [M] [N]
//则其v[i][j][k]元的地址可以写成:(p+i*M*N+j*N+k)
依此类推。
指针数组
数组的元素可以是指针,称一个以同类型的指针为元素的数组为指针数组。
声明
*类型 P[常量表达式]={初值表};
*int p[3]; //说明顺序:p → [3] → * → int。
char *pstr[2]={“123”,“456”}; //p[0]指向字符串"123",p[1]指向字符串"456"
应用
- 用指针数组表示数值型二维数组
- 用指针数组表示字符串数组
多重指针
如果p是指向T类型变量的指针,则p的地址或存储p的地址的变量pp被称为T类型的双重指针或T类型的二级指针。
类似地,ppp则被称为T类型的三重指针或T类型的三级指针。
T ****…***p;
有n颗“*”,p是T类型的n重指针
多重指针主要用作函数的形参.当实参为指针变量的地址时,形参需要用多重指针来表示
声明具有命令行参数的main函数:
在调用main函数时,系统会向它传递两个参数,一个是命令行中字符串的个数,它是一个整型形参,一般称为argc.另一个是字符指针数组,它的每个字符指针元素都指向命令行中用空格分隔的字符串,字符指针数组的名字一般称为argv.在argv数组中,argv[0]指向可执行文件名字符串
int main(int argc,char *argv[]) //即int main(int argc,char **argv)
{… //函数体
}
//argc是整型形参,其值表示命令行中字符串的个数;
//argv是字符指针数组形参,它的每个字符指针元素指向命令行中的一个字符串。
挖坑:如何用传入命令行参数的形式执行这一可执行文件呢?
指针函数
如果函数的返回值是指针类型的值,该函数称为指针函数。
声明
类型 *函数名(形参表);
char *strstr(char *s,char *t);
应用
利用指针函数返回的指针,可以对返回的指针所指向的对象进行进一步的操作。
试想,如果该指针指向一个数组,实际就间接解决了函数返回多值的问题。
常见于字符串的操作函数
函数指针
即指向函数入口地址的指针变量,通过函数指针,可以调用它所指向的函数。
声明
类型 (*函数指针名)(形参表);
*int (pfunc)(int i,int n);
赋值
函数指针名=函数名;
pfunc = function;
调用
(*函数指针名)(实参表); /*间访调用形式*/
pfunc(5,10);
函数指针名(实参表); /*直接调用形式*/
*(pfunc)(5,10);
结构与联合
结构体
现实生活中又存在大量这样一类应用需求,即如何将类型不同而关系又非常密切的数据项组织在一起,统一加以快速处理。
结构体就是这样的基本数据类型的集合
声明一个结构体
1、struct
关于结构体类型的定义的总结;
一般格式就是;
struct 结构体名//(也就是可选标记名)
{成员变量;
}别称名(可省略);//使用分号表示定义结束;struct person
{char *name;int age;double height;
};
若想方便后续定义,我们可以利用typedef+原有数据类型名+别名
。
typedef struct person Sperson;
//或者在申明时同时起别称
typedef struct person
{char *name;int age;double height;
} Sperson; //此处起别称
嵌套结构的声明
具有结构类型成员的结构类型
在声明嵌套结构类型时,应该先声明结构类型成员的结构类型,再声明嵌套结构类型
定义结构体
//定义同时初始化
struct person A = {"SX",17,160.8};
struct person A = {A.name = SX,A.age = 17,A.height = 160.8,
};
//先定义再初始化
struct person B;
B = (struct person){"XS",71,160.8};
//==取用==结构体中的元素:用‘.’运算符
//A.name//定义一个结构体指针
struct person *sip;
sip = &A;
//修改操作(取用元素'->')
sip->age = 99;
sip->name = "aaa";
//等价于(*sip).age = 99
结构类型可以嵌套定义,形成嵌套结构。即一个结构中允许出现其他结构类型的成员。
结构体的定义作用域
在函数内部申明只能在函数的内部使用,所以一般将函数体按需要定义在函数外部。
柔性数组成员
结构类型中最后一个成员可以具有不完全的数组类型,使得C语言可以在有限程度上支持动态结构类型。
即最后一个成员可以声明为一个不给出维大小的数组
struct dy_stu_study { /* dy_stu_study是结构名 */char num[12]; /* 学号成员,字符数组类型 */char name[9]; /* 姓名成员,字符数组类型 */char sex; /* 性别成员,字符类型 */int score[]; /*各科成绩的柔性数组成员,动态整型数组*/
};
访问成员
通过成员选择运算符“.”访问成员。通过“*”用结构指针访问结构变量的成员。结构指针通过成员选择运算符“->”访问结构变量的成员
struct T
{int n;char *pm;
}s={10,"abcdef"},*p=&s;
表达式 | 值与类型 | 执行操作 |
---|---|---|
++p->n | 11, int | 访问n并使其增1 |
p->n++ | 10, int | 访问n,表达式取其原值,再使n增1 |
*p->pm | ‘a’, char | 访问pm,访问所指字符’a’ |
*p->pm++ | ‘a’, char | 访问pm所指字符’a’后pm增1指向’b’ |
*++p->pm | ‘b’, char | 先访问pm,然后pm增1,再访问pm所指字符’b’ |
共用体
**联合体(union)**中是各变量是==“互斥”==的,内存使用灵活,节省内存空间。
union point
{int x;int y;
};
//定义的内部变量共用一块存储单元(以内部占存储空间最大的变量为存储单元)
//每一次访问修改都会清除原本的存储内容
union chi
{int i;char ch[sizeof(int)]; //控制输出的形式
};
内存存储
struct:从第0个属性开始以内部属性的最大内存为单位大小分配内存空间,分配后面属性的内存时也开辟这样的单位大小。一旦不够就会重新分配,并且将当前属性的值直接存储到新分配的存储空间中,之前的剩余的存储空间舍弃。
成员在内存中连续存放
struct person
{char *name; //1int age; //4double height; //8
};
struct person A = {"SX",17,160.8};
//sizeof(A)=16
//若互换位置
struct person
{double height; //8char *name; //1int age; //4
};
//sizeof(A)=16
//再互换位置
struct person
{char *name; //1double height; //8int age; //4
};
//sizeof(A)=24
union:内部变量共用一块存储单元(以内部占存储空间最大的变量为存储单元).
大端模式(Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。(高存低)
小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。(低存低)
- 关键在于理解内存空间的开辟
内存分配
stdlib.h库函数的调用
-
malloc和free
int num; scanf("%d",&num); int *p = (int *)malloc(num*sizeof(int)); //分配内存 free(p); //使用结束后释放内存
malloc在内存的动态分配区域中分配一定长度的连续空间,申请的内存不会进行初始化。
2.calloc
链表
声明一个只含有整数类型和指向自身实例指针的结构类型
定义
一种动态数据结构,由一系列包含数据域和指针域的结点组成。
单向链表
结点的指针域中只包含一个指向后一个结点的指针
Head:头指针
定义一个单向链表
struct s_LIST{int data;struct s_LIST *next;
};
struct s_LIST *head;