预处理指令 都是#开头
包括
#define PI 3.14159 //object-like 类型的宏替换
#define PERIMETER(r) (2*PI*(r)) //function-like 类型的宏替换 (需要加很多括号)
int main();
int main(void);
int main(int argc, char* argv[]);//或者int main(int argc, char **argv);
Windows平台下的命令行解析
main(int argc, char* argv[]) {
int argi, int Nflag = 0;
char *xval = NULL;
for(argi = 1; argi < argc && argv[argi][0] == '-'; argi++){
char *p;
for(p = &argv[argi][1]; *p != '\0' ; p++ ){
switch(*p){
case 'N':
Nflag = 1;
printf("-N seen\n");
break;
case 'x':
xval = argv[++argi];
printf("-x seen (\"%s\")\n",xval);
break;
default:
fprintf(stderr,"unkonwn option -%c\n",*p);
}
}
}
}
const和指针变量配合使用的时候,易混淆
const char* p;
char* const p;
区分方法:
- 画一条垂直线穿过上述指针声明中的 星号(*)位置,
- const在线的左边 指针指向的数据为常量
- const在线的右边 指针本身为常量
能通过编译,但是不能运行,因为没有定义。
#include
int foo(int i);
int main(){
foo(1);
return 0;
}
对定义来说,编译器要做实际的事,无论是变量还是函数,定义时都要分配一段内存,以便把定义的变量和函数装入内存中去。
extern只用于声明变量,不用于声明函数。事实上也可以在函数前面加extern,但是与没有使用extern是等价的
在一个项目中。最好把所有的声明放到一个 统一的global.h文件中。
- 头文件范例
#ifndef GLOBAL_H
#define GLOBAL_H
#include "other.h" //其他头文件
typedef long WORD //类型定义
typedef struct student{
int num;
char name[20];
int age;
} STUD; //类型typedef,但是没有定义一个变量
int f(int i); //函数的声明
extern int foo;// 全局变量的声明
#endif
C++编译文件时,为了支持函数重载,编译器执行一项name mangle 的过程。
- name mangle 编译的时候,会改变函数的名字,链接的时候会出错。
- 为了支持.c 和 .cpp混编,需要使用 extern “C”关键字
用于.c和.cpp文件的通用的 头文件格式
#ifdef __cplusplus
extern "C"{
#endif
sum(int,int);
#ifndef __cplusplus
}
#endif
遵守两个基本原则:
1. C语言的源文件 C++的.cpp文件没必要使用 extern “C” 关键字
2. C或C++混编的项目中,C++要用到C中定义的函数 sum,或者 C要用到C++中定义的 sum,都是需要上面描述的头文件对函数sum进行声明
- extern “C”只对函数有作用,对C++中的类没有任何作用
…
需要使用某个数学功能的时候,先去api找一下
一些字符串判断函数,声明于 ctype.h中
- isalnum
- isalpha
- islower
- isupper
- isspace
处理字符串相关的函数主要包括 String manipulation ,String examination 。
- String manipulation 字符操作
- strcpy
- strcat
- …
- strcpy和strcat会改变传入的字符串的内容
字符串函数发生溢出
#include
int main(){
char src[8] = {"world"};
char des[10] = {"hello"};
strcat(des,src);//溢出
}
String examination 字符检查
…
中间是to:
- atof 转换为双精度浮点数
- atoi 转换为整数
- atol 转换为long类型数
printf的“大表弟”,sprintf() 目测s是string的意思或者是字符数组
- sprintf()把内容输出到第一个参数指定的字符串str中,其余功能都是一样的。
- Java中字符串可以直接加, 不需要这么麻烦
数字换成字符串
int main(){
float f = 1234.56789f;
int i = 123;
char str[20];
sprintf(str,"%d",i);
sprintf(str,"%10.3f",f);
printf("%s\n",str);
}
char sentence[] = "sep. 12 1993";
char month[20];
int day,year;
sscanf(sentence,"%s %d %d",month,&day,&year);
printf("month-> %s\n",month);
printf("day-> %d\n",day);
printf("year-> %d\n",year);
抓住 一个中心,两个基本点。
- 一个中心 指 time_t time(time_t* timer)函数
- 两个基本点 分别为
- 系统时间 time_t
- 通过typedef来定义的,实际上是 long
- 日历时间(或分解时间)struct tm。
两种利用time函数取得系统时间的方法,分别为
1. 利用函数参数,
time_t now;
time(&now);
2. 利用函数的返回值。
now = time(NULL)
#include
#include
int main(void) {
char a[100];
time_t now;
time(&now);//或者 now = time(NULL) ;
printf("%s",ctime(&now));
printf("%d\n",time(NULL) + 60*60*24*7);
strftime(a,100,"%m-%d-%Y",localtime(&now));
printf("%s",a);
}
随机数函数的实现
unsigned long int next = 1;
//伪随机数
int rand(void){
next = next * 1103515245 + 12345;
return (unsigned int)(next/65535) % 32768;
}
//通过给定的种子生成“真正的”随机数
void srand(unsigned int seed){
next = seed;
}
每次调用前先调用srand(time(NULL));将随机数种子赋值为当前time函数运行时候的系统时间。以便产生真正的随机数。
int main(){
srand(time(NULL));
int i;
for(int i = 0; i<5; i++){
printf("%d\t",rand());
}
}
模拟扑克洗牌。使用“交换”的技巧模拟一副扑克洗牌的过程。(牌即为长度为54的)
for(int i = 0; i < 54; i++){
//生成随机数
int c = rand()%54;
while(i+c > 54){
c = rand()%54;
}
//将生成的随机数作为位移,交换
int t = a[i];
a[i] = a[i+c];
a[i+c] = t;
}
生成 0到1之间的随机数
#include
#include
#include
int main(){
srand(time(NULL));
int i;
int rand_int;
float ran_float;
for(int i = 0; i<100; i++){
rand_int = rand();
ran_float = (float)rand_int/RAND_MAX;
printf("%f\n",ran_float);
}
}
eg:
int ary[3];
//高效方法
memset(ary,0,sizeof(ary));
for(int i = 0; i< 3; i++){
printf("%d\n",ary[i]);
}
//复制的高效方法
int b[3];
memcpy(b,ary,sizeof(ary));
C语言的函数不支持用return返回一个数组
下标从零开始
遍历时使用半开半闭的区间访问数组
VS的每一个项目,都可以分别建立debug版本和release版本。release版本,通常不进行边界检查。
- debug版下,当定义一块数组的时候,它会在数组的后面加两个元素,一旦入侵了,这两个元素会引发一个异常(越界三个位以上也还是检查不出来的),无论是报警还是不报警,都发生在debug下。
内存的基本单位是字节(byte),每一个字节都有一个独一无二的地址(它的编号)。
- 内存地址都是16进制表示的。
- 32位机 地址用4个字节表示,最多支持2^32字节内存,也就是4G内存。
- 地址从0x00000000到0xFFFFFFFFF。
- 64位机 的地址占8个字节表示,最多支持2^64字节内存,也就是16G
避免方法,定义一个指针变量的时候带等号,暂时不确定就让它指向NULL。
#include
int main(){
int i = 10;
void *vp = NULL;
int *ip = &i;
int k;
vp = ip;
// vp++;//错误
// k = *vp;//错误
ip = (int*)vp;
ip++; //ok
k = *ip; // ok
}
NULL用来描述指针的值
指针 | 数组 | |
---|---|---|
指针 | int **pp | int *pa[5] |
数组 | 数组指针 int (*ap)[5] | int aa[2][3] |
使用指针型数组的一个优点在于对多个字符串排序,排序的过程并不需要移动真实的字符串,而只需要改变指针的指向
#include
#include
//字符串排序指针数组实现
int main(){
char *temp = NULL;
char *ptr[] = {"Pascal","Basic","Fortran","Java","Visual C"};
for(int i = 0; i< 5; i++){
for(int j = i+1; j < 5; j++){
if(strcmp(ptr[i],ptr[j])>0){
temp = ptr[i];
ptr[i] = ptr[j];
ptr[j] = temp;
}
}
}
for(int i = 0; i<5; i++){
printf("%s\n",ptr[i]);
}
}
换成二维数组的方式实现字符串排序,效率要明显低很多,因为涉及到利用strcpy函数来复制和移动整个字符串
#include
#include
//字符串排序二维数组实现
int main(){
char tmp[10];
char ptr[][10] = {"Pascal","Basic","Fortran","Java","Visual C"};
for(int i = 0; i< 5; i++){
for(int j = i+1; j<5; j++){
if(strcmp(ptr[i],ptr[j])>0){
strcpy(tmp,ptr[i]);
strcpy(ptr[i],ptr[j]);
strcpy(ptr[j],tmp);
}
}
}
for(int i = 0; i<5; i++){
printf("%s\n",ptr[i]);
}
}
如何定义一个指针数组?
有了 int (*p)[3] = ary[2][3]; 二维数组也可以用指针来访问了
C语言的内存映像
*************************
* 命令行数据 *
* 环境变量数据 *
*************************
* 栈( stack ) *
* ***************************** *
* . *
* . *
* *
*--------------------------- --*
* 堆( heap ) * malloc分配的内存,使用完以后必须free
*************************
* 静态存储区 自动初始化为0 *
*************************
* 常量存储区 *
*************************
* 代码段 *
*************************
#include
char *gp = "hello";//常量存储区
char ga[] = "hello";//静态存储区
char *foo(){
char *p = "hello";//常量存储区
char a[] = "hello";//栈
// p[0] = 'z';// 运行时错
// gp[0] = 'z';//运行时错
gp = a;
gp[0] = 'z';
return a;
}
int main(){
char *str = foo();
// str[0] = 'z'; //运行时错
ga[0] = 'z';
return 0;
}
初始化指针时创建的字符串常量与数组中的字符串的区别:
函数指针最常见的一个用处·就是“回调函数”
1. 如何声明?
- int (*pf)(); 指向的就是一个返回值为int类型的函数。函数的参数,声明时并不重要,无须写出。
2. 如何初始化
- pf = &f; 通过取地址运算符来让函数指针指向一个特定的函数f
3. 如何使用?
- pf() 或者 (*pf)() 来调用 这个函数
typedef的主要功能就是帮助定义一个新的数据类型,并给这个新的数据类型一个别名
- 分三步定义出 void (*ap[10]) (void (*)() );
void (*ap[10]) (void (*)() );
//利用typedef定义
typedef void (*pfv)();
typedef void (*pf_taking_pfv) (pfv);
pf_taking_pfv ap[10];
复用typedef定义的新类型:
void (*f()) ();
//利用typedef 定义
typedef void (*pfv)();
pfv f();
C语言是强类型语言,但是C中预定义的数据类型却远远不够。C允许用struct来定义自己的数据类型
实际上struct 就是 所有成员 都是public 的一个class
struct {
...
} stu1,stu2;
结构体的尺寸。
#include
struct {
char c;
int i;
}s;
int main(){
s.c = 'a';
s.i = 0x0a0b0c0d;
printf("%d",sizeof(s));
}
栗子:
Struct E1 {
int a;
char b;
char c
} e1;
第一地址肯定存放a是4Byte地址,第二地址,b要1Byte的地址,来欢迎 公式一登场: 4 == 1*N (N等于正整数) 答”是”!地址现在为5Byte,下一个c要1Byte的地址同上,所以,就是6Byte。来欢迎公式二登场,在这个E1中最大的字节是4,而我们的地址字节是6,4的整数倍不是6,所以,要加2Byte(总地址),So,整个字节为8!
struct {
char *str;
} s1,s2;
int main(){
s1.str == s2.str; //比较指针指向的地址
strcmp(s1.str,s2.str); //比较指针的指向的内容
s1.str = s2.str; // 两个指针指向同一个地址
s1.str = (char*)malloc(100,sizeof(char)) ;//分配空间,指向不同的地址
strcpy(s1.str,s2.str);
free(s1.str);//使用完 释放空间
}
写一个结构体到流fp中
int fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
struct {
//...
} s
fwrite(&s,sizeof(s),1,fp);
向函数传递一个结构体,遵循的是值传递。需要将传入的实参拷贝一次。基于效率的考虑,我们将函数的形参声明为结构体类型的指针。
实现考虑到某一变量 所有可能取的值,那么就可以尽量用含义清楚的单词来表示它的每一个值。按此种方式定义的类型,称为枚举类型(是一种基本数据类型)
- 自己不设置,则默认从0开始赋值
- 枚举符并不是字符串
枚举描述的一个星期的例子
#include
enum day {MON=1,TUE,WEN,THUR,FRI,SAR,SUN};
int main() {
day d = MON;
// d = 1; //错误
d = (enum day)54;//霸王硬上弓
printf("%d\n",d);
// printf("%s\n",d); //错,枚举符并不是字符串
}
FILE 是C中定义的一个结构类型。
- 当使用fopen函数打开一个文件时,会返回一个FILE* (指针)变量。
- 每个FILE指针变量标识一个特定的磁盘文件
- 为什么要返回一个指针呢?
- 因为FILE结构体比较大,所以传回这个结构的首地址明显比复制整个结构体并传回的效率高。
后来的windows 想追求不同,只能用反斜杠了
编程的时候最好使用斜杠 分割路径名。
#include
#include
int main(){
FILE *fp = fopen("file.txt","r+");
int c;
do{
c = fgetc(fp);
if(islower(c) != 0){
fseek(fp,-1L,SEEK_CUR);
fputc(toupper(c),fp);
fseek(fp,0L,SEEK_CUR);//必不可少,输入和输出之间,必须要有一个fseek来进行分割
}
} while(!feof(fp));
}
把一个文件中的小写字母变成大写字母。
- 读文件中的一个一个字符,如果读到小写字母,退一个位置,把它改成大写的,写到文件中(会把小写的给覆盖掉)。
文本文件的行尾标志符定义
- UNIX/Linux下: 用“换行” 表示
- DOS/Windows: 回车+换行,与传统一致。
- Mac : 回车
C内部统一使用’\n’ 来表示断行。
- C遇到’\n’时,C会根据不同的平台调用对应的函数,完成相应的转换,然后把正确的内容写到文件中。
- 在Windows上, ‘\n’被转换为 0x0D 0x0A (‘\n’也被叫做 回车换行符)
- Mac 0x0D
- UNIX 或 Linux 不做转换
- 不仅写出时做这种转换,在读入时,也做相反的转换
在打开模式中增加字母“b”,就是以二进制模式打开文件。”wb”,”rb”
- 为何有两种模式?
- 不同系统有不同的断行符定义 造成的
feof到达文件结尾时返回真,否则返回假
- test.txt文件中只有一个a时
- 以下程序会 输出 97-1,
- -1从何而来?
#include
//feof的用法思考
int main(){
int c;
FILE *fp = fopen("test.txt","r+");
while(!feof(fp)){
c = getc(fp);
printf("%d",c);
}
}
当我们从缓冲区读取一个文件内容的时候,缓冲区的末尾处会添加一个EOF标志。这个EOF标志只出现在缓冲区中,而不出现在硬盘上的文件中。
- 标志位是上一个操作设置
- 如果读操作,如fgets、fgetc、fscanf、fread 读取到了 EOF符的时候,会设置这个标志位。
- 位置指针指向了EOF,但是还没有读到的时候,返回假
//fgets函数的特点
#include
//fgets函数的特点
int main(){
FILE *fp = fopen("test.txt","r+");
char str[100];
while(!feof(fp)){
fgets(str,100,fp);
printf("%s",str);
}
}
假设test.txt只包含abc和一个回车符,以上程序会输出两行abc。
- 原因fgets第二遍读取时,读取到了EOF并且没有其他字符被读入,所以str中的内容不变
鉴于feof函数需要是上一个读操作读取到EOF才为真,应该给它取名为freadeof才对。
几乎所有的读函数,当它们读取到末尾 或 读取出错时,都会返回相同的值。
- 所以这个时候应该分别使用 feof 和 ferror函数
//判断读取到文件末尾
#include
int main(){
int c;
FILE *fp = fopen("test.txt","r+");
while(c = fgetc(fp)){
if(feof(fp)){
break;
}
if(ferror(fp)){
//error handle
}
}else{
//do sth
}
}
第一代的计算机是由许多庞大且昂贵的真空管组成,并利用大量的电力来使真空管发光。可能正是由于计算机运行产生的光和热,通常吸引一些小虫子。如果引得一只小虫子 Bug 钻进了一支真空管内,将导致整个计算机无法工作。所以人们发现计算机不工作了,第一件事就是打开机子看看是否又飞进去了bug。后来,Bug这个名词就沿用下来,表示电脑系统或程序中隐藏的错误、缺陷或问题。 与Bug相对应,人们将发现Bug并加以纠正的过程叫做“Debug”,意即“捉虫子”或“杀虫子”
1947年9月9日,葛丽丝·霍普(Grace Hopper)发现了第一个电脑上的bug。当在Mark II计算机上工作时,整个团队都搞不清楚为什么电脑不能正常运作了。经过大家的深度挖掘,发现原来是一只飞蛾意外飞入了一台电脑内部而引起的故障(如图所示)。这个团队把错误解除了,并在日记本中记录下了这一事件。也因此,人们逐渐开始用“Bug”(原意为“虫子”)来称呼计算机中的隐错。现在在华盛顿的美国国家历史博物馆中还可以看到这个遗稿。
直到目前为止,我们只是在使用C语言,但是并不理解C语言,只知道how,而不知道 why。
- 有的时候,你如何理解C语言并不重要,编译器是如何理解C语言的才是终极答案。
下面的两个函数中
- f1 会打印出一个4(32位机),而不是数组a的长度(因为函数参数 数组变量a 会被编译器 转换为一个 指针变量,所以sizeof(a) 为指针变量的长度)
- f2 中可以对一个“数组变量”进行赋值 也是这个道理
void f1(char a[10]){
int i = sizeof(a);
printf("%d\n",i);
}
int f2(char str[]){
if(str[0] == '\0'){
str = "none";
}
printf("%c",str);
}
一般地,
- C#、java主要用于编写直接面向用户的各种(GUI)应用程序
- C++多用于开发各种后台使用的算法和逻辑库
- C则更底层,主要用于开发更核心的算法和靠近硬件的各种驱动程序库和控制程序。
踏踏实实地一点一点学习,慢慢地融会贯通。
作者的某一位学生(大学开发了各种语言的编译器)的观点。
- 编程语言、工具、所在的系统是什么都没关系,真正有用的算法和设计模式,建议学好的东西。
1. 计算机结构
- 计算机组成原理、体系结构
2. 数据结构
- 每种结构动手实现一下,形成自己的数据结构小类库
3. 操作系统的基本知识
- 不是window,也不是linux,而是 并发、调度、缓存机制、文件系统等算法型的东西
4. 算法 无穷无尽,首先把基础算法弄明白
- eg:动态规划、贪婪、分支限界 此类的经典经典算法