关于在打败C魔王前经常被背刺这件事(C语言常见问题描述及解决方案和原因)

C语言常见bug汇总

异世界前述架构

To be or to be is a question
在异世界里,特别是以代码量等价于代码力的等级划分世界里,行云流水的代码和干净的排版总会大受好评,而能涉及到这些评判准则的前提是,最起码能跑通写出的代码,才能解锁其他花里胡哨的东西,而代码中神奇的bug就是需要被解决的首要问题。
嘿导师说过,“存在就贼拉合理”,所以在解决C魔王,获得offer公主的青睐作为目标的征途中,首要解决的就是bug怪,本文就是debug攻略。

一、声明和初始化 hi

记忆中有个人,每次见面都会说 hi

1、命名

常见关键字

1.数据的选择

  1. 如果需要大数值 (大于 32, 767 或小于 −32, 767), 使用 long 型。否则, 如果空间很重要 (如有大数组或很多结构), 使用 short 型。除此之外, 就使用 int 型。
  2. 如果严格定义的溢出特征很重要而负值无关紧要, 或者你希望在操作二进制位和字节时避免符号扩展的问题, 请使用对应的无符号类型。
  3. 要注意在表达式中混用有符号和无符号值的情况。细化结果如下图
    在这里插入图片描述

2.h文件 or cpp文件

  1. 一个全局变量或函数可以 (在多个编译单元中) 有多处 “声明”, 但是 “定义” 却只能允许出现一次。定义是分配空间并赋初值 (如果有) 的声明。最好的安排是在某个相关的 .c 文件中定义。
  2. 在头文件 (.h) 中进行外部声明, 在需要使用的时候, 只要包含对应的头文件即可。定义变量的 .c 文件也应该包含该头文件, 以便编译器检查定义和声明的一致性。
  3. extern 可以用作一种格式上的提示表明函数的定义可能在另一个源文件中,就是会有牵引性。

3.结构体

typedef struct { 
char *item;  
NODEPTR  next;  
}   *NODEPTR;  

但是编译器报了错误信息。难道在C语言中一个结构不能包含指向自己的指针吗?
其实是可以的,先声明后使用。

struct node {
char *item;
struct node *next;
};
typedef struct node *NODEPTR;

这样的话就可以达到使用目的,但如果喜欢整洁那么最简单的方法就是如下,而且还是等价的

typedef struct node {
char *item;
struct node *next;
}node ,*nodelist;

顺序程序具有顺序性 封闭性和可再现性,不具有并发性
结构化程序要求的是易读性

2、复杂声明

例如定义一个包含 N 个指向返回指向字符的指针的函数的指针的数组?
答案 char ((*a[N])())();
可以用typedef嵌套拆解来理解

typedef char *pc;	/* 字符指针	*/ 
typedef  pc  fpc();	/* 返回字符指针的函数 */ 
typedef fpc *pfpc; /* 上 面 函 数 的 指 针 */ 
typedef pfpc fpfpc(); /* 返回函数指针的函数 */ 
typedef fpfpc *pfpfpc; /* 上 面 函 数 的 指 针 */ 
pfpfpc  a[N];	/* 上面指针的数组 */

划重点,在被使用前,记得声明

3、静态初始化

  1. 具有 “静态” 生存期的未初始化变量 (即, 在函数外声明的变量和有静态存储类型的变量) 可以确保初始值为零, 就像程序员键入了 “=0” 一样。因此, 这些变量如果是指针会被初始化为正确的空指针, 如果是浮点数会被初始化为 0.0。

  2. 这些规则也适用于数组和结构 (称为 “聚合体” ); 对于初始化来说, 数组和结构都被认为是 “变量”。
    malloc函数声明会有脏数据,记得判断null,来提高代码的健壮性。

  3. 内部静态类对象的可见性和生存型static 关键限定其所修饰的全局变量或函数只能在当前源文件中使用或函数,如果你需要在其它源文件中可以共用到,那么你必须将它声明为“外部存储类型”

4、向 p[i] 赋值的时候, 我的程序崩溃了

  1. Q: 以下的初始化有什么区别?
    char a[] = “string literal”;
    char *p= “string literal”;
  2. A:字符串常量有两种稍有区别的用法。用作数组初始值 (如同在 char a[] 的声明中), 它指明该数组中字符的初始值。其它情况下, 它会转化为一个无名的静态字符数组, 可能会存储在只读内存中, 这就是造成它不一定能被修改。
    在表达式环境中, 数组通常被立即转化为一个指针,因此第二个声明把 p 初始化成指向无名数组的第一个元素。

5、函数指针的声明方法

用下面这样的代码
extern int func();
int *demon() = func;
当一个函数名出现在这样的表达式中时, 它就会 “蜕变” 成一个指针 (即, 隐式地取出了它的地址), 这有点类似数组名的行为。

二、结构、联合和枚举

1、typedef struct and struct

  1. Q:声明 struct x1 { . . . }; 和 typedef struct { . . . } x2; 有什么不同?
  2. A:第一种形式声明了一个 “结构标签”; 第二种声明了一个 “类型定义”。主要的区别是在后文中你需要用 “struct x1” 引用第一种, 而用 “x2” 引用第二种。也就是说, 第二种声明更像一种抽象类新 —– 用户不必知道它是一个结构, 而在声明它的实例时也不需要使用 struct 关键字。
    且一个结构可以包含指向自己的指针。

2、模拟继承等面向对象程序设计特性

把函数指针直接加入到结构中就可以实现简单的 “方法”。你可以使用各种不雅而暴力的方法来实现继承, 例如通过预处理器或含有 “基类” 的结构作为开始的子集, 但这些方法都不完美。很明显, 也没有运算符的重载和覆盖 (例如, “导出类” 中的 “方法”), 那些必须人工去做。
显然的, 如果你需要 “真” 的面向对象的程序设计, 你需要使用一个支持这些特性的语言, 例如 C++

3、内存分配技巧

char namestr[MAXSIZE];
MAXSIZE 比任何可能存储的 name 值都大。但是, 这种技术似乎也不完全符合标准的严格解释。这些 “亲密” 结构都必须小心使用, 因为只有程序员知道它的大小, 而编译器却一无所知。

4、接受结构参数的函数传入常数值

复合常量” (compound literals); 复合常量的一种形式就可以允许结构常量。
例如, 向假想 plotpoint() 函数传入一个坐标对常数, 可以调用
plotpoint((struct point){1, 2});
与 “指定初始值” (designated initializers) 结合, 也可以用成员名称确定成员值:
plotpoint((struct point){.x=1, .y=2});

5、读取文件

  1. 用 fwrite() 写一个结构相对简单:
    fwrite(&somestruct, sizeof somestruct, 1, fp);
  2. 对应的 fread() 调用可以再把它读回来。但是这样写出的文件却不能移植 。同时注意如果结构包含任何指针, 则只有指针值会被写入文件, 当它们再次读回来的时候, 很可能已经失效。最后, 为了广泛的移植, 你必须用“b” 标志打开文件;
  3. 移植性更好的方案是写一对函数, 用可移植 (可能甚至是人可读) 的方式按域读写结构, 尽管开始可能工作量稍大。
  4. 下面是输出输入库的图表介绍
    在这里插入图片描述
    在这里插入图片描述
  5. C++源程序文件的缺省扩展名为cpp,编译而成的目标文件的缺省扩展名为obj,可执行扩展名为exe。
    C++程序的基本单位是函数,基本模块也是函数

6、sizeof

为了确保分配连续的结构数组时正确对齐, 结构可能有这种尾部填充。即使结构不是数组的成员, 填充也会保持, 以便 sizeof 能够总是返回一致的大小。

7、初始化联合、枚举

  1. “指定初始值”, 可以用来初始化任意成员。
  2. 枚举的一些优点: 自动赋值; 调试器在检验枚举变量时, 可以显示符号值; 它们服从数据块作用域规则。(编译器也可以对在枚举变量被任意地和其它类型混用时, 产生非重要的警告信息, 因为这被认为是坏风格。)一个缺点是程序员不能控制这些对非重要的警告; 有些程序员则反感于无法控制枚举变量的大小。

三、表达式 expression

1、输出其旧值之后才会执行运算

  1. Q:使用我的编译器,下面的代码 int i=7; printf("%d\n", i++ *i++); 返回 49?不管按什么顺序计算, 难道不该打印出56吗?
  2. A:尽管后缀自加和后缀自减操作符 ++ 和 – 在输出其旧值之后才会执行运算, 但这里的“之后”常常被误解。没有任何保证确保自增或自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进行。也不能保证变量的更新会在表达式 “完成”

2、&& 和 || 运算符

while((c = getchar())!= EOF && c != ’\n’) 
  1. 这些运算符在此处有一个特殊的 “短路” 例外: 如果左边的子表达式决定最终结果 (即,真对于 || 和假对于 && ) 则右边的子表达式不会计算。因此, 从左至右的计算可以确保, 对逗号表达式也是如此。而且, 所有这些运算符 (包括 ? : ) 都会引入一个额外的内部序列点
  2. 如果在复杂的优先级里,有可以绝对结果的话,后面花里胡哨的就不会再考虑。

3、计算 or类型

为什么如下的代码 int a = 100, b = 100; long int c = a * b; 不能工作

  1. 根据 C 的内部类型转换规则, 乘法是用 int 进行的, 而其结果可能在转换为long 型并赋给左边的 c 之前溢出或被截短。可以使用明确的类型转换, 强迫乘法以 long 型进行:
  2. long int c = (long int)a * b;
    注意, (long int)(a * b) 不能达到需要的效果。
    当两个整数做除法而结果赋与一个浮点变量时, 也有可能有同样类型的问题, 解决方法也是类似的

四、指针

1、我想声明一个指针并为它分配一些空间, 但却不行。这些代码有什么问 题 ?char *p; *p = malloc(10)

  1. 你所声明的指针是 p, 而不是 *p, 当你操作指针本身时 (例如当你对其赋值, 使之指向别处时), 你只需要使用指针的名字即可:
    p = malloc(10);
    当你操作指针指向的内存时, 你才需要使用 * 作为间接操作符:
    *p = ’H’;

2、*p++ 自增 p 还是 p 所指向的变量

  1. 后缀 ++ 和 – 操作符本质上比前缀一目操作的优先级高, 因此 p++ 和(p++) 等价, 它自增 p 并返回 p 自增之前所指向的值。
  2. 要自增 p 指向的值, 使用(*p)++, 如果副作用的顺序无关紧要也可以使用 ++*p。

3、char * 型指针正巧指向一些 int 型变量, 我想跳过它们。为什么如下的代码((int *)p)++;

  1. 在 C 语言中, 类型转换意味着 “把这些二进制位看作另一种类型, 并作相应的对待”; 这是一个转换操作符, 根据定义它只能生成一个右值 (rvalue)。而右值既不能赋值, 也不能用 ++ 自增。
  2. (如果编译器支持这样的扩展, 那要么是一个错误, 要么是有意作出的非标准扩展。) 要达到你的目的可以用:
    p = (char *)((int *)p + 1);

4、修改 找地址

参数是通过值传递的。被调函数仅仅修改了传入的指针副本。你需要传入指针的地址 (函数变成接受指针的指针), 或者让函数返回指针。

5、能否用 void** 指针作为参数,使函数按引用接受一般指针

  1. C 中没有一般的指针的指针类型。void* 可以用作一般指针只是因为当它和其它类型相互赋值的时候, 如果需要, 它可以自动转换成其它类型; 但是, 如果试图这样转换所指类型为 void* 之外的类型的 void** 指针时, 这个转换不能完成。
  2. 举一个栗子:
    必须先定义一个临时变量, 然后把它的地址传给函数:
    int five = 5;
    f(&five);

6、静态数据成员

  1. 静态数据成员必须在体外初始化,它是类的所有对象的共有成员,需要使用类名使用,静态数据成员的初始化与控制权限无关
  2. 类的数据成员不能在声明类的时候初始化;
    比如class A中的static属性 a
    则A:a=~;
    (ps:对象指针是不调用函数的)

五、NULL

  1. 空指针的典型用法: 表示 “未分配” 或者 “尚未指向任何地方” 的指针。
    根据语言定义, 在指针上下文中的常数 0 会在编译时转换为空指针。也就是说, 在初始化、赋值或比较的时候, 如果一边是指针类型的值或表达式, 编译器可以确定另一边的常数 0 为空指针并生成正确的空指针值。
可以使用未加修饰的 0需要显示的类型转换
初始化函数调用, 作用域内无原型
赋值变参函数调用中的可变参数
比较
固定参数的函数调用且在作用
内有原型

有两条简单规则你必须遵循:

  1. 当你在源码中需要空指针常数时, 用 “0” 或 “NULL”。

  2. 如果在函数调用中 “0” 或 “NULL” 用作参数, 把它转换成被调函数需要的指针类型

六、数组和指针

1、源文件中定义了 char a[6], 在另一个中声明了 extern char*a 。为什么不行

  1. 在一个源文件中定义了一个字符串, 而在另一个文件中定义了指向字符的指针。extern char * 的申明不能和真正的定义匹配。类型 T 的指针和类型 T 的数组并非同种类型。请使用 extern char a[ ]。
    也许会说 char a[]和char *a是一样的,但其实数组不是指针。
  2. 数组定义 char a[6] 请求预留 6 个字符的位置, 并用名称 “a” 表示。也就是说, 有一个称为 “a” 的位置, 可以放入 6 个字符。而指针申明 char *p, 请求一个位置放置一个指针, 用名称 “p” 表示。这个指针几乎可以指向任何位置: 任何字符和任何连续的字符, 或者哪里也不指
    在这里插入图片描述
  3. 那句话只是说数组和指针的算法定义可以用指针方便的访问数组或者模拟数组。
    由于数组会马上蜕变为指针, 数组事实上从来没有传入过函数。允许指针参数声明为数组只不过是为让它看起来好像传入了数组, 因为该参数可能在函数内当作数组使用。特别地, 任何声明 “看起来象” 数组的参数。

2、数组和指针的地址的区别

  1. 数组自动分配空间, 但是不能重分配或改变大小。指针必须明确赋值以指向分配的空间 (可能使用 malloc), 但是可以随意重新赋值 (即, 指向不同的对象), 同时除了表示一个内存块的基址之外, 还有许多其它的用途。
  2. 由于数组和指针所谓的等价性, 数组和指针经常看起来可以互换, 而事实上指向 malloc 分配的内存块的指针通常被看作一个真正的数组(也可以用 [ ] 引用)。

3、arr数组 与&arr

两者的区别在于类型。
在标准 C 中, &arr 生成一个 “T 型数组” 的指针, 指向整个数组。在 ANSI 之前的 C 中, &arr 中的 & 通常会引起一个警告, 它通常被忽略。在所有的 C 编译器中, 对数组的简单引用(不包括 & 操作符)生成一个 T 的指针类型的指针, 指向数组的第一成员。

4、多维动态数组

#include <stdlib.h>
int **array1 = malloc(nrows * sizeof(int *)); 
for(i = 0; i < nrows; i++)
 array1[i] = malloc(ncolumns * sizeof(int));

如果我这样写 int realarray[10]; int *array = &realarray[-1]; 我就可以把 “array” 当作下标从 1 开始的数组。

七、内存分配

1、get一家

  1. 用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符。 若遇到输入流中的文件结束符,则函数值返回文件结束标志EOF(End Of File),一般以-1代表EOF,用-1而不用0或正值,是考虑到不与字符的ASCII代码混淆,但不同的C ++系统所用的EOF值有可能不同。
char c;
c=cin.get(),
cout.put(c)
第二种格式
cin.get(ch)
    cin.get(字符数组, 字符个数n, 终止字符)
或
    cin.get(字符指针, 字符个数n, 终止字符)
//其作用是从输入流中读取n-1个字符,赋给指定的字符数组(或字符指针指向的数组),
//如果在读取n-1个字符之前遇到指定的终止字符,则提前结束读取。如果读取成功则函数返回true(真),
//如失败(遇文件结束符) 则函数返回false(假)。
  1. C++ getline()函数读入一行字符
    getline函数的作用是从输入流中读取一行字符,其用法与带3个参数的get函数类似。即
    cin.getline(字符数组(或字符指针), 字符个数n, 终止标志字符)

2、我的 strcat() 不行.我试了 char *s1 = "Hello, "; char *s2 =“world!”; char *s3 = strcat(s1, s2);结果奇怪

  1. 主要的问题是没有正确地为连接结果分配空间。C 没有提供自动管理的字符串类型。C 编译器只为源码中明确提到的对象分配空间 (对于字符串, 这包括字符数组和串常量)。程序员必须为字符串连接这样的运行期操作的结果分配足够的空间, 通常可以通过声明数组或调用 malloc() 完成。
  2. strcat() 不进行任何分配; 第二个串原样不动地附加在第一个之后。因此, 一种解决办法是把第一个串声明为数组:
    char s1[20] = "Hello, ";

3、返回字符串或其它集合的争取方法是什么呢?

返回指针必须是静态分配的缓冲区 , 或者调用者传入的缓冲区, 或者用 malloc() 获得的内存, 但不能是局部 (自动) 数组。

4、malloc与free

  1. 指针和它所指向的东西是完全不同的。局部变量在函数返回时 就会释放, 但是在指针变量这个问题上, 这表示指针被释放, 而不是它所指向的对象。
  2. 用 malloc() 分配的内存直到你明确释放它之前都会保留在那里。一般地, 对于每一个 malloc() 都必须有个对应的 free() 调用。

八、字符串和字符

1、为什么 strcat(string, ’!’); 不行

  1. 字符和字符串的区别显而易见, 而 strcat() 用于连接字符串。
  2. C 中的字符用它们的字符集值对应的小整数表示。字符串用字符数组表示; 通常你操作的是字符数组的第一个字符的指针。二者永远不能混用。要为一个字符串增加 !, 需要使用
    strcat(string, “!”);

2、字符串是否跟某个值匹配

  1. C 中的字符串用字符的数组表示, 而 C 语言从来不会把数组作为一个整体操作 (赋值, 比较等)。上面代码段中的 == 操作符比较的是两个指针 —— 指针变量
  2. string 的值和字符串常数 “value” 的指针值 —— 看它们是否相等, 也就是说, 看它们是否指向同一个位置。它们可能并不相等, 所以比较决不会成功。
  3. 要比较两个字符串, 一般使用库函数 strcmp():

3、char 字符串

  1. Q:char a[] = “Hello, world!”; 为什么我不能写 char a[14]; a = “Hello, world!”;
  2. A:字符串是数组, 而你不能直接用数组赋值。可以使用 strcpy() 代替:
    strcpy(a, “Hello, world!”);

4、ASCII转

数字字符和它们对应的 0-9 的数字之间相互转换时, 加上或减去常数 ’0’, 也就是说, ’0’ 的字符值。

九、预处理器

1、一般用途宏交换两个值

  1. 如果希望这个宏用于任何类型 (通常的目标), 那么它不能使用临时变量, 因为不知道需要什么类型的临时变量 (即使知道也难以找出一个名字), 而且标准C 也没有提供 typeof 操作符。
  2. 最好的全面解决方案可能就是忘掉宏这回事, 除非你还准备把类型作为第三个参数传入。
  3. 下面是宏的注意事项
    在这里插入图片描述
  4. 宏名没有类型,其参数也没有类型。宏不存在类型问题,宏名无类型,它的参数也无类型,只是一个符号代表,展开时代入指定的字符串即可。宏定义时,字符串可以是任意类型的数据。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换。

2、放置位置

  1. 作为一般规则, 你应该把这些东西放入头 (.h) 文件中:
    • 宏定义 (预处理 #defines)
    • 结构、联合和枚举声明
    • typedef 声明
    • 外部函数声明
    • 全局变量声明
  2. 当声明或定义需要在多个文件中共享时, 尤其需要把它们放入头文件中。特别是, 永远不要把外部函数原型放到 .c 文件中。
    另一方面, 如果定义或声明为一个 .c 文件私有, 则最好留在 .c 文件中。

3、10.4 #include <> 和 #include “” 有什么区别

<> 语法通常用于标准或系统提供的头文件, 而 “” 通常用于程序自己的头文件。用 <> 括起来的头文件会先在一个或多个标准位置搜索。用 “” 括起来的头文件会首先在 “当前目录” 中搜索, 然后 (如果没有找到) 再在标准位置搜索。

10、ANSI/ISO标准

1、const int n = 5; int a[n]

  1. const 限定词真正的含义是 “只读的”; 用它限定的对象是运行时 (同常) 不能被赋值的对象。因此用 const 限定的对象的值并不完全是一个真正的常量。
  2. 在这点上 C 和 C++ 不一样。如果你需要真正的运行时常量, 使用预定义宏 #define,最好是使用动态命名。

2、const char *p” 和 “char * const p

  1. Const为常数不可能改,所以必须在命名的时候进行初始化。
  2. 常量指针:不能改变指向内容的变量(即不能改变指针保存的内容)。可以不初始化const double *p;内容不可变,但是指针自己可以改变指向另一个常量。
  3. 插入一个容易误解的栗子 :Const *double p为常指针必须得初始化。
  4. 指针常量: int *const ConstPtr=&a;//必须初始化,只能指向一个变量,绝不可再改变指向另一个变量。指针本身是一个常量,内存地址是常量,但里面的地址所指向的内容可以变换

3、&与*

  1. C++ 提供了两种指针运算符,一种是取地址运算符 &,一种是间接寻址运算符 *。
    指针是一个包含了另一个变量地址的变量,您可以把一个包含了另一个变量地址的变量说成是"指向"另一个变量。变量可以是任意的数据类型,包括对象、结构或者指针。
  2. & 是一元运算符,返回操作数的内存地址。例如,如果 var 是一个整型变量,则 &var 是它的地址。该运算符与其他一元运算符具有相同的优先级,在运算时它是从右向左顺序进行的。
    您可以把 & 运算符读作"取地址运算符",这意味着,&var 读作"var 的地址"。
    第一个功能是引用,用来传值,出现在变量声明语句中位于变量的左边,
    第二个功能是用来获取首地址,在给变量赋初值的时候后出现在等号的右边作为一元运算符的出现,取地址
  3. 第二个运算符是间接寻址运算符 ,它是 & 运算符的补充。 是一元运算符,返回操作数所指定地址的变量的值。
  4. Void指针就是已经定义但未初始化的指针,例题就是对比,不然只是h,用void化为了地址
    传递参数时,若形参是指*针,则实参对应是地址引&用;若形参是地址引用,对应实参是值;
    静态变量和外部变量的初始化是在编译阶段完成的,而自动变量的赋值是函数调用

4、 我能否把 main() 定义为 void, 以避免扰人的 “main无返回值”

  1. 不能。main() 必须声明为返回 int, 且没有参数或者接受适当类型的两个参数。如果你调用了 exit() 但还是有警告信息, 你可能需要插入一条冗余的 return 语句 (或者使用某种 “未到达” 指令, 如果有的话)。
  2. 把函数声明为 void 并不仅仅关掉了警告信息:它可能导致与调用者(对于main(), 就是 C 运行期初始代码) 期待的不同的函数调用/返回顺序。
    注意, 这里讨论的 main() 是相对于 “宿体” 的实现; 它们不适用于 “自立” 的实现, 因为它们可能连 main() 都没有。但是,自立的实现相对比较少,如果你在使用这样的系统, 你也许已经知道了。如果你从没听说过它们之间的不同, 你可能正在使用 “宿体” 的实现, 那我们的讨论就适用。

5、pragma once

#pragam 指令提供了一种单一的明确定义的 “救生舱”, 可以用作各种 (不可移植的) 实现相关的控制和扩展: 源码表控制、结构压缩、警告去除 (就像 lint 的老 /* NOTREACHED */ 注释), 等等

十一、标准输入输出库

1、char c; while((c =getchar()) != EOF)

  1. 保存 getchar 的返回值的变量必须是 int 型。getchar() 可能返回任何字符值, 包括 EOF。
  2. 如果把 getchar 的返回值截为 char 型, 则正常的字符可能会被错误的解释为 EOF, 或者 EOF 可能会被修改 (尤其是 char 型为无符号的时候), 从而永不出现

2、我如何在 printf 的格式串中输出一个 ’%’?我试过 %, 但是不行

只需要重复百分号: %%。%不行, 因为反斜杠 \ 是编译器的转义字符, 而这里我们的问题最终是 printf的转义字符。

3、为什么 char s[30];scanf("%s", s); 不用 & 也可以

需要指针; 并不表示一定需要 & 操作符。当你向 scanf() 传入一个指针的时候, 你不需要使用 &, 因为不论是否带 & 操作符, 数组总是以指针形式传入函数的。

4、我用 scanf %d 读取一个数字, 然后再用 gets() 读取字符串, 但是编译器好像跳过了 gets() 调用

scanf %d 不处理结尾的换行符。如果输入的数字后边紧接着一个换行符, 则换行符会被 gets() 处理。
作为一个一般规则, 你不能混用 scanf() 和 gets(), 或任何其它的输入例程的调用; scanf 对换行符的特殊处理几乎一定会带来问题。要么就用 scanf() 处理所有的输入, 要么干脆不用。

十二、库函数

  1. C++一个类可以从直接或者间接的祖先中继承所有属性和方法,采用如是可以提高软件的可重用性。
  2. C++中封装性、继承性和多态性是面对对象程序设计的三个主要性质。若含有不同类型操作数,只会向高级转换。
  3. 庞大的函数就不再做一个一个解释,推荐一个 查询小软件 c++API.chm,做算法比赛入门的时候通过这个查询可以解决很多问题。

十三、封装函数

1、派生和继承

1.1派生

  1. 派生构造由内向外,析构由表及内,抽象类中的虚函数不执行,派生里有执行函数最好,如果也没有那么就还是抽象类,直到可以实现为止,const走const函数对应的。
  2. 抽象类可以简单理解为 有纯函数的类。

1.2继承

  1. 继承是会继承对应基类中的属性,但是访问权限会有所不同和更改,比如三种访问,继承了基类的私有成员,但不能直接访问,可以通过派生类的友元函数去访问基类的私有成员。
  2. 图表展示对比记忆一下
  3. 继承里函数都是可访问public和protect但是private不管谁都不行,下面推荐一个容易理解的方法。
    筛眼理解法,+成员函数可以访问基类的public和protect成员,实例变量根据筛眼归类到派生类中,且仍然只能用基类public里的东西,除了友元其他不可窥探,并且派生的友元也不可以访问

1.3构造(深浅)

在这里插入图片描述

  1. 拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。
  2. 浅复制是简单赋值在同一个区域
  3. 深复制是开辟了新的动态存储空间,并且将数据也进行复制进去。
    需要调用拷贝构造函数:
    函数的参数是类的对象;
    函数的返回值是类的对象;
    定义一个新的对象,用另一个对他初始化
  4. 通过派生类的构造函数调用基类的构造函数有两种方式,隐式和显式两种。
    1. 隐式方式就是在派生类的构造函数中不指定对应的基类的构造函数,这个时候调用的是基类的默认构造函数(即含有默认参数值或不带参数的构造函数。
    2. 显式方式,就是在派生类的构造函数中指定要调用的基类的构造函数,并将派生类构造函数的部分参数值传递给基类构造函数。
    3. 注:除非基类有默认的构造函数,否则必须采用显式调用方式多态性
      建立一个有成员对象的派生类对象时,各构造函数体的执行次序为基类、成员对象类、派生类

借用一个试题加强一下认知
在这里插入图片描述

拷贝构造函数:也称复制构造函数,当对象的参数是另一个对象时,调用。一般的形式为:
class  类名
{
类名(形参参数){构造函数的声明}
类名(类名&对象名){拷贝构造函数的声明}}
class  ClassName
{
ClassName(int x){  };
ClassName( ClassName&obj){  };
}

基类A的派生类是B,classB : publicA{}, 有这样一个定义:A *p=new B();
那么,p只能调用基类中的成员函数,不能调用派生类才有的成员函数,但是如果基类中存在虚函数,那么p调用的是派生类中与虚函数对应的函数。

2、友元函数

  1. 在class box里有friend,所以有friend的函数可以使用class里面的任何成员
    在这里插入图片描述
  2. 友元函数的定义既可以在类内部进行,也可以在类外部进行。他提高了程序的运行效率,但与此同时破坏了类的封装性和隐藏性,使得类的非成员函数可以 访问类的私有成员

3、Class

  1. 程序中的typename都可以替换成class; 但是class不可以替换成typename;通常和template一块使用。
  2. 引用静态成员函数两种方法:
//类名::静态成员函数名(Constants::getPI()),  
//对象名.静态成员函数名(object.getPI());
在类外创建构造函数,成员函数;
A::A(){};
void A::count(){ };
在类外静态变量要初始化:
int  A::age=12;

在这里插入图片描述
在函数里,B *ptr是因为在主函数中,B *ptr=new D;已经有拿着b的名字用d构建,且是又指针的。而 B&ref是根据确实的找地址,地址是啥申明就是啥,而B b和就是B中的函数执行,不涉及继承

3、虚函数 and 内敛函数

3.1、虚函数

  1. 抽象类是指含有纯虚函数的类,不能够实例化对象。但可以定义指向抽象类数据的指针变量,当派生类成为具体类后。就可以用这种指针指向派生类的对象,然后通过该指针调用虚函数,实现多态性的操作
  2. 纯虚函数在申明函数的时候被初始化为0的函数,定义时无函数体,在末尾要加=0;
    若用数组名作为函数调用的实参,传递给形参的是数组的首地址。
    虚函数必须是基类的非静态成员函数,其访问权限可以是public和protected
    虚函数:在基类中用virtual定义,在派生类中再次定义的函数,不得是静态成员函数;
    编译时的多态性是通过函数的重载和模板体实现的,运行时的多态性是通过虚函数体实现的

在这里插入图片描述

3.2、内敛函数

  1. 一般函数进行调用时,要将程序执行权转到被调用函数中,然后在返回调用它的函数中,而内敛函数调用时,是将调用表达式用内敛函数体来替换,类似之前提到的宏。
  2. 内联函数是在对源程序中的其他成分编译之前进行的。

在这里插入图片描述

4、虚基类

虚基类解决了间接二义性
在这里插入图片描述

5、函数重载和运算符的重载

  1. c++不能重载的运算符只有5个 分别是 " . “、” .* “、” ::"、“?:”、“sizeof”。
    在这里插入图片描述
  2. 运算符重载的时候会省略一个参数,所以如果是一元运算符则只有一个参数;一元运算符可以作为成员函数和非成员函数。
  3. 二元运算符作为非成员函数,必须有两个参数;
  4. 运算符函数:可以定义为类的友元函数和成员函数;
  5. 友元函数重载时,参数列表为1,说明是1元,为2说明是2元。必须以类的成员函数重载
运算符重载为成员函数格式:
    <函数类型> operator <运算符>(<参数表>)
    {
     <函数体>
    }
运算符重载为类的友元函数的一般格式为:
    friend <函数类型> operator <运算符>(<参数表>)
   {
     <函数体>
    }
当运算符重载为类的成员函数时,函数的参数个数比原来的操作数要少一个,友元函数参数个数无变化;
成员函数重载时,参数列表为空,是一元,参数列表是1,2元。函数继承问题和友元

十三、End

上面的内容建议有一定的c基础上学习和理解
文章中提出的一些遇到的bug和学习过程中容易出现的误区。
这些总结都是在做题和自己写代码中得出来的。
还是多做多写自然会发现问题,研究透彻了,之后便是大路一条。
        That is all ,it is my pLeasure!
                       by icelei

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

磊哥哥讲算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值