程序设计与算法基础¶
约 5894 个字 154 行代码 9 张图片 预计阅读时间 22 分钟
指针¶
关于指针的一些声明¶
-
NULL是⼀个预定定义的符号,表示0地址 -
指向不同类型的指针是不能直接互相赋值的;不同类型的指针也不能比较大小!相同的指针可以比大小/相减,但是不可以相加!
-
void *是不知道指向什么东西的指针,相当于它只是单纯地保存了地址,而没有保存这个地址上元素的类型(当地址被分配以后,它的类型就已经确定了)。所以说对于void *,可以进行的操作只有:暂时保存某个指针的地址、把它的地址赋给另外一个元素类型正确的指针变量。任何涉及类型的运算void *都不能执行。 -
指针的作用:
1)需要传入较大的数据时用作参数
2)传入数组后对数组做操作
3)函数返回时不止一个结果
4)需要用函数来修改不止一个变量
5)动态申请内存
-
开指针操作:
Node * ptr=(Node*)malloc(sizeof(Node));int *a=(int*)malloc(sizeof(int)*n)
1.数组和指针的关系¶
-
数组名类型可以理解为一个封装了长度信息以及取址有所差别的
const void *对于取址的差别,即如果对数组
a进行了一次取址操作,那么指针p的类型是int (*)[],而非int **;但如果再取一次,那么系统就不管了,类型就是指向指针的指针。对于封装了长度信息,即数组和指针的
sizeof结果是不一样的。当不涉及长度和取址的时候,数组退化成
const void *;当把数组作为参数传入函数时,数组会退化成指针,即使参数变量取名时int []。 -
在开一个 \(n\) 的数组的时候,相当于预留了连续的 \(n\) 个地址。
所以就有 \(a[k]=*(a+k)\)
2.指针的优先级¶
\(*\) 本质上是解引用,也就是说把一个指向的值给解了,变成最原始的指针。
也就是说,p 在没有和 * 结合前都是值,一旦结合就是指针,指向剩下还没结合的类型。
括号 \(()\) 的优先级最高,其次是数组 \([]\) ,然后是剩余的 \(*\) ,然后是类型。
正确的添加限定¶
需要注意的是,只有出现在 * 与标识符之间的限定才会被赋予指针类型。举例来说,语句 char * const p; 声明了一个类型为 char * 的、值不可变的指针 p,它指向的对象类型为 char,值可通过指针 p 改变;而语句 const char * p; 声明了一个类型为 const char * 的、值可变的指针 p,它指向的对象类型为 const char,值不可通过指针 p 改变。
3.有关指针和数组的 sizeof¶
在 C 语言中,输出 size_t 元素的类型为 %zd;
4.二维数组的地址开法¶
比如你开一个 int a[3][3] 的二维数组,那么计算机会给你分配九个 int 的地址空间。
此时数组 \(a\) 会被转换成 int (*)[3] 类型的指针(和 int ** 是不同的数据类型),指针指向的类型是 \(int[3]\)。即 \(a\) 是指向第一个 int [3] 数组,随后 a+i 是向右移动了 \(i*sizeoof(int[3])\) 个字节。
*(a+i) 也可以看成 a[i],那么下一层就可以改成 a[i]+j,即从 a[i] 指向的地址开始,向右移动 \(j*sizeof(int)\) 个字节,指针指向的类型是 \(int\)。
由此我们可以看出,其实 \(a[1][-1]\) 其实是指向 \(a[0]\) 的最后一个元素。
5.函数指针¶
函数指针的值,实际上是函数第一条指令的地址,也即函数的入口地址。
函数的内存布局¶
在可执行文件中,指令与数据被分别存储在代码段与数据段中。每个函数的指令往往被存储在代码段中的一段连续内存空间中,根据这一特点,我们可以将函数看作一种特殊的对象。
C 语言允许通过调用函数指针来调用函数
也就是说,函数和函数指针是一回事情,所以你从 (&fun)() 到 (******fun)() 都是可以的,但是不能 (&fun_ptr)(),因为你调用函数指针的指针,鬼知道他是不是函数的指针。
6.有关 typedef¶
https://zhuanlan.zhihu.com/p/637324207?utm_id=0
可以这么定义。
7.有关野指针¶
就是在定义指针的时候你不知道把它指到哪去了,所以也自然不能把值往野指针的地址里面塞。
程算被拷打记录¶
一些易错题¶
-
After executing the following code fragment, the value of variable
mis:answer: 10
-
以下代码,语法正确的是:
answer: C
explanation:循环体在语法层面也是一个语句;所以结尾的分号是必备的(要么就是大括号,所以
do ; while (1);也是合法语句),表示这个语句执行结束了。而循环体另外一个重要的元素就是循环条件,所以while里面必须填东西、for里面必须有(;;)。 -
在C语言程序中,若对函数类型未加显式说明,则函数的隐含类型为
answer:
int -
正确的输出为:
answer: 3 8
-
若函数调用时的实参为变量,下列关于函数形参和实参的叙述中正确的是()。
A.函数的实参和其对应的形参共占同一存储单元
B.形参只是形式上的存在, 不占用具体存储单元
C.同名的实参和形参占同一存储单元
D.函数的形参和实参分别占用不同的存储单元
answer: D
explanation:形参在执行函数的时候,会有属于它的存储单元;实参在外面也有它的存储单元(哪怕是一个
expression,也会有一个存储单元对应这个expression的结果) -
以下说法正确的是:
A.一个C语言源文件(.c文件)必须包含main函数
B.一个C语言源文件(.c文件)可以包含两个以上main函数
C.C语言头文件(.h文件)和源文件(.c文件)都可以进行编译
D.在一个可以正确执行的C语言程序中,一个C语言函数的声明(原型)可以出现任意多次
answer: D
explanation:.c 文件不一定要包含 main 函数,多文件编译的时候肯定有程序不带 main 函数;函数声明即给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)。函数原型可以多次出现。
-
数组名作为参数时,以下叙述正确的是( )。
A.函数调用时是将实参数组的所有元素值传给形参。
B.函数调用时是将实参数组的第一个元素值传给形参。
C.函数调用时是将实参数组的首地址传给形参。
D.函数调用时是将实参数组的所有元素的地址传给形参。
answer: C
-
关于递归函数以下叙述错误的是( )。
A.递归函数是自己调用自己。
B.递归函数的运行速度很快。
C.递归函数占用较多的存储空间。
D.递归函数的运行速度一般比较慢。
answer: B
explanation:占用较多空间——栈空间
-
这道题很重要(隐式转换)
For the function declaration
void f(char ** p),the definition __ ofvarmakes the function callf(var)incorrect。A.
char var[10][10];B.
char *var[10];C.
void *var = NULL;D.
char *v=NULL, **var=&v;answer: A
explanation:

-
For definitions:
char s[2][3]={"ab", "cd"}, *p=(char *)s;the expression __ is correct and its value is equivalent to the elements[1][1].A.
*(s+3)B.
*s+2C.
p[1][1]D.
*++p+2answer: D
-
以下正确的是:
A.f 函数头改为
int f(int *a, int n),无需其它改变,效果完全一样。B.
main函数中增加int *p=a; 将f(a,n);改为f(p,n);,效果完全一样。C.用数组名作
f()的参数,则main()中的数组与f()中的数组是同一数组。D.用数组名作
f()的参数,实现了实参与形参双向的值传递。answer: D
explanation:所谓的双向传递,就是在函数内修改形参的值,也会改变外面实参的值。但事实上参数类型是一个指针,形参只是新开了一个内存空间取保存传入的那个地址,所以原先的实参指针所代表的地址并不会变化。
-
结构体成员的类型必须是基本数据类型()
answer:F
explanation:可能也是结构体
C 语言标识符(identifier)(W1)¶
自定义的名称,用于表示变量、函数、数组等程序实体,必须由字母、数字、下划线组成,且首位不能是数字。a$b_c 也是标识符。
while 括号内 --k 还是 k--(W1)¶
对于程序:
输出为:
对于程序:
输出为:
k-- 还是 --k 决定了是先拿 while 判断 k 再运算,还是先运算 k 再判断。
对于程序:
这里 i++ 和 ++i 是一样的,但是要注意 sum 的初始值赋值了没。
运算优先级(W1)¶

-
其中单目运算符、赋值运算是右结合;赋值运算的运算优先级很低很低!
-
四则运算大于移位运算大于关系运算大于位运算(与大于异或大于或)大于条件运算大于赋值运算。
-
对于关系类运算符,运算顺序是左结合,就是从左到右一个个算。
-
逗号表达式:从左到右依次计算结果,并返回最右边的那个式子的结果;逗号的优先级是所有表达式最低的,比赋值运算还低。
-
对于
&&:左边是False就不算了;对于||:左边是True就不算了
有关浮点数(W2)¶
为什么浮点数叫浮点数?——在一个定点数上加一个阶码,阶码用于移动小数点位置。
float 和 double的区别:

float 和 double 的默认输出都是保留 6 位小数
有关循环体(W3)¶
- 循环体如包括有一个以上的语句,则必须用一对大括号{}括起来,组成复合语句,复合语句在语法上被认为是一条语句。
有关 switch(W3)¶
先找到和条件相同的数值,随后依次往下进行(即如果该 case 没有 break ,则会继续进行下一个 case)
如果没有数值,就找 default,如果没有 default,那么这个 switch 无效
关键字(keyword)、操作符(operator)和函数的区别(W4)¶
- 操作符(
static operator,如sizeof)会在编译阶段直接处理计算。
而函数则会在代码段中以 call 的形式去调用(代码运行时)
- 操作符不需要任何头文件都能运行,因为其是由编译器直接实现(或者说他的实现是由编译器完成的)
而函数的实现是在C语言库中实现的
- 程序运行时,会先将函数的参数入栈,然后再跳到函数的代码段运行,而操作符直接在原地计算
关键字:如 int,double 等已被语言使用的词
有关函数(W5)¶
-
Function is a piece of code, which takes zero or many parameters, does one thing and returns zero or one value.
-
()is the evidence of a function call -
形参(
former parameter)和实参(real parameter)的区别:
-
Local are to the execution rather than the function.: 函数内的变量不是以函数为单位定义的,而是每执行一次就新分配内存空间,执行完释放。 -
函数可以嵌套调用但不能嵌套定义。
-
函数声明:只是说明有这个函数(函数里面的参数类型),不占用空间;函数定义:有函数体。一个程序同一个函数只能有一次定义,但是可以有多次声明。
-
C 语言 main 函数的参数:
main(int argc, char **argv)其中
argc表示编译命令的变量参数;argv则表示了这些变量的名称
有关递归(W7)¶
-
递归的4个基础 :
Base Case:⾄少存在⼀种情况,不再进⼀步递归就可以直接得到结果
Make Progress:每⼀次递归都是向着base case前进
Always Believe:始终相信递归是可⾏的
Compound Interest Rule:不要在不同的递归调⽤中做相同的计算
有关排序(W8)¶
十种排序方法的具体分析及图示: https://www.cnblogs.com/onepixel/articles/7674659.html
-
快速排序
-
注意冒泡排序的尾部优化
时间复杂度:和逆序对个数有关。(\(\sqrt{逆序对个数} * n\))?
-
时间复杂度测试程序:
有关字符串(W11)¶
-
C 语言中的字符串其实是一个以终止字符结尾的字符数组
\0 -
字符串的大小计入中止字符,而其长度不计入。
如 "" 为大小为 1 的字符数组,唯一元素是 \0. 一个 char 占用 1 个字节
-
数组的大小可以比字符串字面量的大小少 1,此时中止字符将被忽略。举例来说,语句
char str[3] = "abc";等价于语句char str[3] = {'a', 'b', 'c'};,它不是一个字符串。合格的字符串必须结尾是
\0,如char str[4]="abcd",那么它就等价于语句char str[4]={'a','b','c','\0'};对于字符指针char *,只有当它所指的字符数组有结尾的\0,才能说它所指的是字符串)。\0不算在字符长度判断里,但是算在字符串大小里。如果中止字符被忽略,那么就会一直输出字符串直到读到中止字符。
-
\b意味着光标左移一格;\r意味着把光标移到行首。 -
以下内容非常很重要!
对于
char *s="Hello",s是一个指针,初始化指向字符串的第一个字母。但注意,在这种情况下,"Hello"它是一个不可变字符串,所以s的类型是const char *,也就意味着不能改变字符串中的任意一个元素。但对于数组
char s[]="Hello",那就没有问题(在这个定义语句中,决定了s的长度)。在编译程序的时候,字符串
"Hello"不会改变(可以只看成一个文本,在可执行文件中保存了,在用.txt打开可执行文件的时候,这个字符串也会原封不动地出现)。后者是把他赋值到了一个可变的数组上,但前者那种情况只是把s指了过去,指向这个文本所在的位置,而这个文本是存储在静态存储区上的。或者说它不是程序运行时所开的空间——只有这些才可以被改变。它是编译之后就存在的文本——可读不可写,不能改变值。在
printf("%s",s)时,秉持着输出字符串的原则,传入参数是一个char *,从这个地址一直往后输出输出到\0为止(所以说字符数组必须要有\0才能算是字符串)。 -
scanf(“%7s” , string);最多读入七位;printf("%7d\n",x);输出长度为 7. -
char *string; scanf(“%s” , string);不行,因为你不知道string开了多大,读入可能溢出。 -
字符数组的不同定义:

-
一些字符串函数的用法:(
string.h库)int strcmp(const char* str1, const char* str2);比较两个字符串是否相等,相等返回 0 ,\(str1>str2\) 返回 \(> 0\) ,小于则返回 \(<0\) 。char * strcpy ( char * destination, const char * source );把后者复制到前者,一直到\0。char * strncpy ( char * destination, const char * source, size_t n);把source前n个复制。void * memcpy( void * destination, const void * source, size_t n);n:要拷贝多少字节。char *strcat(char *dest, const char *src);把后者移到前者末端,要有足够空间。char *strchr(const char *str, int c);查找c出现的第一个位置,返回指针(intASCII)。char *strstr(const char* s2,const char* s1);查找str出现的第一个位置,返回指针。int putchar(char c);返回c的 ASCII 码;int getchar();返回int是为了读入失败时返回EOF(-1)
可变数组(W13)¶
有关数据类型(W14)¶
sizeof: char 1; short 2(-32768~32767); int=long 4; float: 4 (8位有效); double 8(16位有效)。
double 存储分配:

-
bool所在库:<stdbool.h> -
int取决于 CPU;但是long是固定的四个字节。
原码、反码和补码(W14)¶
原码:开头为符号位,后面均为数值位。
若为正数,原码=反码=补码;
若为负数,则反码为原码除了符号位每一位取反,补码为反码+1;
两个数相加,在计算机里面就是补码二进制相加。
- -128 补码(对于八位来说)为
10000000;原码和反码的可表示区间都是-127~127,因为 0 被表示了两次。 - 补码:\(-2^n +\) 后面非符号位的数值,即对于负数反过来表示。
有关进制和\转义(W14)¶
- A literal with a leading 0 is an octal(8进制)
- A literal with a leading 0x is a hexadecimal(16进制)
%ofor octal,%xfor hexadecimal\x后面接 0-9 A-F 字符通过十六进制来表示一个字符。不过并不会有长度限制,比如'\x00000041'也是一个字符,同样遇到范围外的字符就结束。- 后面可以接最多三个数字来使用八进制表示一个字节,且遇到 0-7 范围之外的数字会结束当前字节,比如
'\101'也表示 'A',而'\08'因为 8 超过了八进制范围,所以这是两个字符放到了一个单引号里,不是正确的用法,如果写成字符串,则"\08"表示两个字符。
编译预处理指令(宏)(W14)¶

#开头的是编译预处理指令
它们不是C语⾔的成分,但是C语⾔程序离不开它们
-
#define⽤来定义⼀个宏(#define <名字> <值>);在C语⾔的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
-
宏定义不存在类型问题,宏名无类型,它的参数也无类型。
-
如果⼀个宏的值中有其他的宏的名字,也是会被替换的
如果⼀个宏的值超过一行,最后一行之前的行末需要加
\ -
带参数的宏的规则——一切都要括号(整个值要括号,参数出现的每个地方都要括号,没有可能会出问题)
#define RADTODEG(x) ((x) * 57.29578)#define MIN(a,b) ((a)>(b)?(b):(a)) -
可以只
#define flag这样,因为还有一种语句叫做#ifdef -
#define语句最好还是别带分号: -
部分宏会被
inline函数替代:inline相当于在编译的时候,在函数调用的地方将直接原地展开,就是类似于#define。内联(
inline)是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。使用方法:不是在声明函数的时候加
inline,而是在定义的时候加。
多文件编程(W14)¶
-
编译指令:
gcc <a.c> <b.c> ... -o me -
编译过程:其实就是把所有函数入口都先准备好,所有全局变量都先开好,然后找到
main开始运行。 -
#include是""还是<>:""要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找;<>让编译器只在指定的目录去找 -
编译器自己知道自己的标准库头文件在哪,环境变量和编译器命令行参数也可以指定寻找头文件的目录
-
一些反直觉的东西:(好像只是在 Windows 中的垃圾 dev 里面有)

-
不对外公开函数:在函数前面加上
static就使得它成为只能在所在的编译单元中被使用的函数(编译单元:一个文件) -
一般来说,头文件(
.h) 放声明不放定义(是规则不是法律);最好把所有要声明的都给声明了;多文件编译过程中,如果要用外部的变量,必须提前声明。(提前incldue或者用extern)
变量的声明(W14)¶
int i; 是变量的定义; extern int i; 是变量的声明
声明是不产生代码的东西;定义是产生代码的东西。
文件操作(W15)¶
-
printf/scanf的返回值:输出/读入字符数 -
while()一直读入如何判断结束:getchar读到了EOF、scanf 返回小于要求读的数量。 -
fopen(const char *path, const char *mode);path是文件名,mode是打开文件的方式 -
r 以只读方式打开文件,该文件必须存在。 r+ 以可读写方式打开文件,该文件必须存在。 w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。 w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
-
需要一个
FILE*型的指针接收fopen的返回值,拿着这个返回值就可以调用相应的接口对文件进项操作。 -
读入失败:
FILE* p=NULL;关闭:fclose(p); -
格式化输入输出(printf,scanf)还有两种变体:sprintf,sscanf 第一个参数为字符串(char*),会输出到字符串中 / 从字符串中读取输入;fprintf,fscanf 第一个参数为文件指针(FILE*),会输出到文件中 / 从文件中读取输入。
有关“链接错误”(W15)¶
链接错误有两种类型:
1)缺定义。当程序中出现对某个外部对象的使用,而连接程序找不到对应的定义时,将产生这个错误。(有声明但是没有定义) 2)重复定义。当被连接的各个部分中出现某个名字的多个定义时,将产生这个错误。
e.g. 
答案应该是链接错误(题目错了应该 include 一下 stdio.h)。
因为 include 相当于文本替换,多文件编译的时候你会发现 include 了两遍 header.h 导致 school 被定义了两次。
——所以这就是强调 .h 里面不要放定义,最好只放声明。
#include<stdio.h> 两次不会出现问题也是因为它里面只有声明。
有关存储类别(W15)¶
1.自动变量(auto变量)¶
函数中的局部变量,如果不专门声明为(static)静态存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。
形参与函数中定义的局部变量,都是自动变量,在调用结束就会自动释放这些存储空间
2.静态变量(static变量)¶
(1)静态局部变量属于静态存储类别,在静态存储区分配存储单元,在程序整个运行期间都不释放。
(2)如果在定义局部变量时不赋初值,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符'\0'(对字符变量)。
(3)虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。因为它是局部变量,只能被本函数引用。
并且,加了 static 后只能在本编译单元内使用。
3.寄存器变量(register变量)¶
一些变量使用频繁,存取变量要花费不少时间,提高效率,允许把局部变量放在CPU中的寄存器中,直接从寄存器中存取,可以提高执行效率,这种变量叫做寄存器变量
以上 3 种局部变量的存储位置是不同的:自动变量存储在动态存储区,静态变量存储在静态存储区、寄存器存储在CPU中的寄存器中。
4.外部变量(extern变量)¶
从其它编译单元来的变量;所以这个 extern 也只是个声明而非定义。