程序的执行
如果变量没有初始化,就直接拿到右边去用,会出现什么呢?
得到了一个非常奇怪的结果,这是因为,在内存当中,我们有一个变量 i 我们没有给它一个初始值,那么,它正好在内存当中,在什么地方,那个地方原本有一些什么样的值在里头,它就是那个值了。
读整数
scanf("%d", &price);
scanf和printf中,f表示format格式化的意思。
常量(C99)
const int AMOUNT = 100
。#include
int main()
{
const int AMOUNT = 100;
int price = 0;
printf("请输入金额(元):");
scanf("%d", &price);
int change = AMOUNT - price;
printf("找您%d元。\n", change);
return 0;
}
const(C99)
%lf
对应double。
注意这里优先级为1的单目运算符+
、-
注意,a=6
这个式子本身,是有结果的,也就是6。
视频中4分20秒开始出现Dev C++断点调试的使用方法。
视频中5分30秒开始,出现了对scanf的讲解。
出现在scanf格式字符串里面的东西,是它一定要你输入的东西,而不是它会给你看的东西。
(学习笔记:能用大括号就用大括号,避免歧义。)
(以下代码利用了switch-case的某个特性)
(注意:while结尾要有分号)
(计算之前先保存原始的值,后面可能有用。)
(判断素数)
(上面的代码中,设置一个标志位的写法,我认为值得借鉴。)
(判断素数的另一种写法。)
#include
int main()
{
int x;
int one, two, five;
int exit = 0;
scanf("%d", &x);
for (one=1; one<x*10; one++)
{
for (two=1; two<x*10/2; two++)
{
for (five=1; five<x*10/5; five++)
{
if (one + tw0*2 + five*5 == x*10)
{
printf("可以用%d个1角加%d个5角得到%d元\n",
one, two, five, x);
exit = 1;
break;
}
}
if (exit == 1) break;
}
if (exit == 1) break;
}
}
(
我主要对这里的先设置一个标志int exit = 0;
,然后满足某种条件后,改变标志exit = 1
,进而改变程序控制流程的写法,感兴趣。
)
(这一段的视频值得重新一看,整数正序分解,因为C 语言中没有趁手的工具,所以这里实现起来略繁琐。)
辗转相除法求最大公约数
(水仙花数编程实现,值得一看)
(关键词:素数、质数)
每一项的分子是前一项分子与分母的和,分母是前一项的分子。
(下面的代码值得一看)
sizeof
sizeof(int)
sizeof(i)
55.6.1.2 数据类型:整数类型
整数
当我们在说1台计算机的字长的时候,我们指的是寄存器是多少宽的,也就是说,这个寄存器是几个bit的。比如说,当我们说寄存器是32个bit的,每一个寄存器可以表达32个bit的数据,同时也是在说,CPU和RAM之间在总线上传递数据的时候,每一次的传递是32个bit,也就是说,当要从内存RAM取数据到CPU里面去,每一次就会要取32个bit。
除了32,现在更常见的是64个bit。
字长在C语言中,反映为int。int表达的是1个寄存器的大小。所以,在不同的平台、CPU上面,int会不一样大。
所以,为什么我们要在计算机的内部使用补码呢?
最大的好处就是,如果你有了一个补码,你用补码来表示这个-1,那么,当你在做加法的时候,你不需要根据条件,去变换,把加+变成减-,你直接拿它去做普通的二进制的加法,你就会得到你想要的那个结果。
整数越界
整数是以纯二进制方式进行计算的,所以:
- 11111111 + 1 –> 100000000 –> 0
- 01111111 + 1 –> 10000000 –> -128(这里我存在疑问)
- 10000000 + 1 –> 01111111 –> 127
下面的代码似乎可以解答我的疑问:
-2^(32-1) —— 2^(32-1)-1
(关键词:无符号数、unsigned
这整段视频建议重新细看、加强理解。)
(如下的代码,建议看视频的详细解释)
(inf:正负的无穷大
nan:表示不是一个有效的数字
这段视频建议再去详细看。)
科学计数法
#include
int main()
{
double ff = 1234.56789;
printf("%e,%f\n", ff, ff);
return 0;
}
输出:
1234568e+03,1234.567890
#include
int main()
{
double ff = 1E-10;
printf("%E,%f\n", ff,ff);
return 0;
}
输出:
1.000000E-10,0.000000
#include
int main()
{
printf("%f\n", 12.0/0.0);
printf("%f\n", -12.0/0.0);
printf("%f\n", 0.0/0.0);
return 0;
}
$ gcc q.c
$ ./a.out
inf
-inf
-nan
(即转义字符)
bool
#include
;条件运算符
count = (count > 20) ? count - 10 : count + 10
;(8分01秒开始出现了“单步进入”,讲解了调用函数时,进入函数内部、单步调试)
函数的先后关系
void sum(int begin, int end)
{
int i;
int sum = 0;
for (i=begin; i<=end; i++)
{
sum += i;
}
printf("%d到%d的和是%的\n", begin, end, sum)
}
int main()
{
sum(1,10);
sum(20,30);
sum(35,45);
return 0;
}
像这样把sum()写在上面,是因为:
void sum(int begin, int end); // 声明。这里是函数原型
int main()
{
sum(1,10);
sum(20,30);
sum(35,45);
return 0;
}
void sum(int begin, int end) // 定义。
{
int i;
int sum = 0;
for (i=begin; i<=end; i++)
{
sum += i;
}
printf("%d到%d的和是%的\n", begin, end, sum)
}
C语言在调用函数时,只能传值给函数。
当我们在main()函数中,做swap(a,b)的时候,是把a的值5交给了swap里的a、把b的值6交给了swap里的b。swap里的a、b,与main里的a、b完全没有任何关系的。
我们在swap里面,对a、b做的任何事情,是swap里的参数a、b的事情,和main的a、b没有任何关系。如上的代码,不能交换a、b的值。
73.7.2.3 函数的参数和变量:本地变量(局部变量)
(关键词:本地变量,也叫做局部变量,因为英文是local,可翻译为本地或局部。
)
也有的地方把它叫做自动变量,把它叫做自动变量是和我们后面讲的一件事情有关系,它的生存期和作用域的关系,把它叫做自动变量是因为,它的生存期是自动的。
(这一段,建议借助视频讲解和在Dev C++编译器中断点调试,可以辅助加强理解context等概念的含义。
关键词:生存期、作用域
从1分59秒开始看。
从4分09秒开始看。)
本地变量的规则
(整段视频都值得重新仔细看,介绍了作用域、生存期,可以结合Python的LEGB同时理解。
特别是8分39秒开始的地方,依次介绍了类似嵌套作用域、变量掩盖之类的概念。)
76.8.1.2 数组:数组的使用
在内存中,数组的单元是依次排列的,而且是紧密依次排列的。
通常来说,用到数组的程序,都需要这么一些环节。
(以下值得一看)
(判断素数、质数)
(求解素数的另外一种思路的算法。)
(伪代码)
为什么变量会有地址?
因为 C 语言的变量,是放在内存里的。每 1 个变量,比如说 int ,可能是 4 个字节,在内存中要占一定的地方,放在某个地方,就有 1 个地址。 & 运算符就是把地址拿出来。
(整段视频都值得重新看,
特别是6分38秒开始的,
关键词:C语言的内存模型、变量的地址、内存中的堆栈stack、自顶向下、分配变量。
)
#include
int main(void)
{
int a[10];
printf("%p\n", &a);
printf("%p\n", a);
printf("%p\n", &a[0]);
printf("%p\n", &a[1]);
return 0;
}
输出:
$ gcc q.c
$ ./a.out
0x7ffe025df0d0
0x7ffe025df0d0
0x7ffe025df0d0
0x7ffe025df0d4
(最后2行,p为int型指针,q为int型、不是指针。)
当我们说, p 指向了 i,实际的意思是,p 的值是变量 i 的地址。
#include
void f(int *p);
void g(int k);
int main(void)
{
int i = 6;
printf("&i=%p\n", &i);
f(&i);
g(i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n", p);
printf("*p=%d\n", *p);
}
void g(int k)
{
printf("k=%d\n", k);
}
输出:
$ gcc w.c
$ ./a.out
&i=0x7ffd4447a5d4
p=0x7ffd4447a5d4
*p=6
k=6
这就意味着,通过 *p
这个指针,我们访问到了 p 所指的 int i 里面的值。
增加一行代码 *p = 26;
:
#include
void f(int *p);
void g(int k);
int main(void)
{
int i = 6;
printf("&i=%p\n", &i);
f(&i);
g(i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n", p);
printf("*p=%d\n", *p);
*p = 26;
}
void g(int k)
{
printf("k=%d\n", k);
}
输出:
$ gcc w.c
$ ./a.out
&i=0x7ffd8ecfb624
p=0x7ffd8ecfb624
*p=6
k=26
k=26 意味着,在经历了 f 函数的调用了之后, i 的值被改了。
地址值 &i
被传进了函数 f(int *p)
,这里,仍然是值的传递,因为传进来的是地址,所以,通过这个地址,在函数内部,可以以这种方式,访问到外面的 i 变量。
因为 p 的值,就是 i 的地址。 *p
就代表了 i 。
当我们做 *p = 26
这个运算的时候,我们实际做的事情,是对 i 做的。
(函数参数里的数组是指针。
从视频0分59秒开始看起)
(“*ar
、ar[]
在函数参数表中出现,是等价的”)
(const 常量)
上图。如果指针是const,比如int * const q = &i;
,在这种情况下,q这个指针是const,它的意思是说,q的值不能被改变,什么是q的值?就是i的地址,就是q指向了i这个事实不能被改变,也就是q不能再指向别人了,它们之间的关系是永久的。
(对上图和上上图的一点说明:)
p可以指向别人,i可以被赋以别的值,但是通过p去修改i就不可以。
如果const在*
前面,那么,表示它所指的东西不能被修改;
如果const在*
后面,那么,表示指针不能被修改;
当我们给一个指针加1的时候,它不是在地址值上加1,它在地址值上加一个sizeof(那个指针所指的类型)
。
所以对指针做一个加1的动作,意味着,我们要把它移到下一个单元去。
(当做两个指针相减的时候,它给你的是两个地址的差除以sizeof(它的类型),也就是,在这两个地址之间,有几个这样类型的东西在,或者说,能放几个这样类型的东西。)
(0分52秒开始,略为完整地介绍了动态内存分配malloc、内存释放free()。
第12行:
malloc返回的结果是是void *
,而a是int*
,所以我们还要类型转换一下。(在前面加上(int*)
)。
)
(第8行:每次申请100兆的空间,然后把申请到的空间,交给p,(p=malloc(100*1024*1024))
中,对p做了一个赋值。赋值也是个表达式,有个结果,结果就是p得到的malloc的那个结果。
while ( (p=malloc(100*1024*1024)) ){
cnt++;
}
这样的代码同时做了2件事情:
1.把malloc的结果赋给了p这个变量;
2.要让p得到的这个值,拿来做while的条件。
如果p得到的地址不是0,那就意味着它得到了一个有效的地址,那么,我们循环要继续,要让cnt去加加。
如果它得到的地址是0,那么while就要退出来。
)
(着重:首地址)
(在字符数组的最后一个元素,使用'\0'
)
(0分28秒开始。
关键词:程序的代码段、只读、保护机制。)
(作为参数,数组的形式和指针的形式是一样的,因为,数组传进去,也是指针,所以,我们在这里全部用指针的形式来表达了。)
size_t strlen(const char *s)
的参数中,const char *s
的前面有 1 个const
,含义是,希望函数strlen()
不修改传进去数组char *s
。
(4分01秒开始,对strcmp的重新实现,值得一看。
包括数组实现和指针实现。
)
(5分20秒开始,分析并重新实现了strcpy,有数组、指针版本。)
(上 面的代码中,main函数外的struct date { sth. }
是在声明一种新的结构类型,main函数内的struct date today = sth.
是在定义这种结构类型的一个结构变量。
)
(数组变量的名字,就是数组的地址。)
(结构数组)
(上面,Date是别名。)
#include
int f(void);
int gAll = 12;
int main(int argc, char const *argv[])
{
f();
f();
f();
return 0;
}
int f(void)
{
static int all = 1;
printf("in %s all=%d\n", __func__, all);
all += 2;
printf("agn in %s all=%d\n", __func__, all);
return all;
}
输出:
$ gcc q.c
$ ./a.out
in f all=1
agn in f all=3
in f all=3
agn in f all=5
in f all=5
agn in f all=7
(
个人理解:
这里的 静态本地变量 ,让我联想起了 Python 中的 生成器 和 yield
。
有时间一定要去读 《 Python 源码分析》,了解下生成器的内部实现。
)
如果有 1 个函数,你要让它返回 1 个指针,那么,你如果返回的是本地变量的地址,这是危险的。
因为一旦离开这个函数,本地变量就不存在了。
#include
int* f(void);
void g(void);
int main(int argc, char const *argv[])
{
int *p = f();
printf("*p=%d\n", *p);
g();
printf("*p=%d\n", *p);
return 0;
}
int* f(void)
{
int i=12;
return &i;
}
void g(void)
{
int k = 24;
printf("k=%d\n", k);
}
我的输出:
$ gcc q.c
q.c: In function ‘f’:
q.c:19:9: warning: function returns address of local variable [-Wreturn-local-addr]
return &i;
^
$ ./a.out
Segmentation fault (core dumped)
视频中的输出:
。。。
1 warning generated.
*p=12
k=24
*p=24
*p
又变成 24 了,但这个过程中,没有人对 *p
和 i
做任何事情,但是 p
所指的那个地方却变成了 24 了。
你可以自己改一点这里的程序,如果在 f
函数里打印出 i
的地址,在 g
函数里打印出 k
的地址,你会发现它们是一样的。
返回 1 个本地变量的地址,让外面的程序继续使用它是有风险的。因为这个函数结束以后,那个本地变量的地址会被继续分配给别人去使用的。
因为,全局变量或静态本地变量的地址和函数没关系,它们是全局生存期的。
返回在函数内 malloc
的内存是安全的,但是容易造成问题
最好的做法是返回传入的指针
#include
#define PI 3.14159
#define FORMAT "%f\n"
#define PI2 2*PI // pi * 2
#define PRT printf("%f ", PI); \
printf("%f\n", PI2)
int main(int argc, char const *argv[])
{
printf(FORMAT, PI2*3.0);
PRT;
return 0;
}
输出:
$ gcc q.c
$ ./a.out
18.849540
3.141590 6.283180
像函数的宏
#define cube(x) ((x)*(x)*(x))
宏定义的结尾不要加上分号 ;
。
比如说函数、全局变量。
在头文件中方的不是声明,而是定义,会造成问题。
#include
int main(int argc, char const *argv[])
{
printf("%9d\n", 123);
printf("%-9d\n", 123);
printf("%+9d\n", 123);
printf("%+-9d\n", 123);
printf("%-+9d\n", 123);
printf("%-+9d\n", -123);
printf("%-9d\n", -123);
printf("%09d\n", 123);
return 0;
}
输出:
#include
int main(int argc, char const *argv[])
{
printf("%9d\n", 123);
printf("%-9d\n", 123);
printf("%+9d\n", 123);
printf("%+-9d\n", 123);
printf("%-+9d\n", 123);
printf("%-+9d\n", -123);
printf("%-9d\n", -123);
printf("%09d\n", 123);
return 0;
}
#include
int main(int argc, char const *argv[])
{
printf("%9.2f\n", 123.0);
return 0;
}
输出:
$ gcc q.c
$ ./a.out
123.00
printf("%9.2f\n", 123.0);
中,9.2f
的含义是,输出 1 个浮点数,后面是 2 位小数,前面的空位、整数位、小数点位加上小数位,一共是 9 位。
#include
int main(int argc, char const *argv[])
{
printf("%*d\n", 6, 123);
return 0;
}
输出:
$ gcc q.c
$ ./a.out
123
printf("%*d\n", 6, 123);
中 *
意思是, 6 是用来满足这个 *
的, 6 会被填到 *
里面去; 123 是用来满足这个 d
的。
所以,输出包括前面的 3 个空格加后面的 3 位数,总共占据 6 个位置。
#include
int main(int argc, char const *argv[])
{
int num;
printf("%drf%n\n", 12345, &num);
printf("%d\n", num);
return 0;
}
输出:
$ gcc q.c
$ ./a.out
12345rf
7
printf("%drf%n\n", 12345, &num);
中, %n
的含义是,当 printf
执行到这个地方的时候,已经输出了多少个字符,然后填到 &num
指针所指的那个变量里面去。
#include
int main(int argc, char const *argv[])
{
int num;
scanf("%*d%d", &num);
printf("%d\n", num);
return 0;
}
输出:
$ gcc q.c -o test
$ ./test
123 345
345
说明:把前面的 123 跳过,然后读到了 345 。 *
的意思是跳过。
#include
int main(int argc, char const *argv[])
{
int num;
scanf("%i", &num);
printf("%d\n", num);
return 0;
}
输出:
$ gcc q.c -o test
$ ./test
123
123
$ ./test
0x12
18
$ ./test
012
10
$ gcc q.c -o test
$ ./test
12345
12345
$ ./test > 12.out
12345
$ cat 12.out
12345
$ cat > 12.in
12345(备注:这里可能需要输入 Ctrl + D 终止。)
$ cat 12.in
12345
$ ./test < 12.in
12345
$ ./test < 12.in > 12.out
$ cat 12.out
12345
这是叫做程序运行时的重定向。
#include
int main(int argc, char const *argv[])
{
FILE *fp = fopen("12.in", "r");
if ( fp ) {
int num;
fscanf(fp, "%d", &num);
printf("%d\n", num);
fclose(fp);
} else {
printf("无法打开文件\n");
}
return 0;
}
$ gcc q.c -o test
$ ./test
12345
$ rm 12.in
$ ./test
无法打开文件
(视频中,这里有一段代码,建议看视频。)
(视频中,有代码,建议看视频。)
对 1 个数,做 2 次异或,就翻回去了。
#include _NODE_H_
#define _NODE_H_
typedef struct _node {
int value;
struct _node *next;
} Node;
# endif
断点调试
Dev C++的断点调试
参考文献:
1. C语言 - 翁恺 - 浙江大学。