--- title: "13-C与C++" created: 2026-08-29 tags: - 基础与理论 - 信息基础大赛 --- # 第 240~300 题:C、C++ > 📚 本文是 [[00-信息基础大赛|信息基础大赛]](10.29 备赛)的第 13 篇,题目以截图为主、文字为点拨。 ## 易错题集 ![[image766.png]] ![[image767.png]] ![[image768.png]] ![[image769.png]] ![[image770.png]] ![[image771.png]] ![[image772.png]] ![[image773.png]] ![[image774.png]] ![[image775.png]] ![[image776.png]] ![[image777.png]] ![[image778.png]] ## 文件相关 使用头文件中提供的函数。 文件指针: 在C中,文件操作需要使用文件指针来跟踪文件的位置和状态。FILE类型是用于表示文件指针的结构体类型。可以使用fopen()函数打开文件并获得文件指针,使用fclose()函数关闭文件。 ```c FILE *file = fopen("example.txt", "r"); // 打开文件 // ... fclose(file); // 关闭文件 ``` 文件打开模式: 打开文件时,需要指定打开文件的模式,常见的模式包括: "r":只读 "w":写入(如果文件存在则截断,如果文件不存在则创建) "a":附加到文件末尾(如果文件不存在则创建) "b":二进制模式 可以通过在模式后添加"b"来进行二进制文件操作,例如:"rb"表示以二进制只读模式打开文件。 ![[image779.png]] ![[image780.png]] 文件读取函数: 使用fgetc()逐字符读取文件。 使用fgets()逐行读取文件。 使用fread()以二进制方式读取数据。 ```c int ch = fgetc(file); // 逐字符读 char buffer[100]; fgets(buffer, sizeof(buffer), file); // 逐行读 ``` ![[image781.png]] 文件写入函数: 使用fputc()逐字符写入文件。 使用fputs()逐行写入文件。 使用fwrite()以二进制方式写入数据。 ```c fputc('A', file); // 逐字符写 fputs("Hello, World!", file); // 逐行写 ``` 格式化输入输出: 使用fprintf()和fscanf()可以进行格式化的输入和输出,类似于printf()和scanf(),但指定文件流。 ```c int value = 42; fprintf(file, "The value is: %d\n", value); fscanf(file, "The value is: %d", &value); ``` 检查文件末尾:feof() —— 已到末尾返回非零值(通常 1),否则返回 0。 ```c if (feof(file)) { printf("End of file reached\n"); } ``` ![[image782.png]] 错误处理: 使用perror()函数可以打印文件操作相关的错误信息。 使用ferror()函数可以检查文件操作是否出错。 ```c if (ferror(file)) { perror("File operation error"); } ``` 文件定位:fseek() 定位到指定位置,ftell() 获取当前文件指针位置。 ```c fseek(file, 0, SEEK_SET); // 定位到文件开头 long position = ftell(file); // 获取当前位置 int fseek(FILE *stream, long offset, int origin); // stream:文件指针;offset:相对 origin 的偏移字节数(可正可负) // origin 基准:SEEK_SET 文件开头 / SEEK_CUR 当前位置 / SEEK_END 文件末尾(正数越过末尾) ``` ![[image783.png]] ## 位与布尔 ### 位运算 c语言被称作最接近底层的语言 原因之一是它提供很多接近底层的操作 比如 它支持按位进行布尔运算 在c语言中 &表示按位的与 |表示按位的或 ~表示按位取反 ^表示按位的异或 <<表示左移 >>表示右移 确定一个位级表达式的结果的最好的方法 就是把16进制的参数拓展成二进制表示 并执行2进制运算 然后再转换回16进制 ```text ~0x41 —> ~[0100 0001] —> [1011 1110] —> 0xBE ``` ### 按位与(&) 两个数x,y做与 在对应的位上面进行布尔运算 ![[image784.png]] ![[image785.png]] #### 妙用 (1)如果希望让某一位或某些位为0 ![[image786.png]] 就可以 `x & 0xFE` FE 的最后一位是0 所以不管 x 的最后一位是什么 结果都是0;FE 前面的 7 位都是1 所以 x 的前 7 位是什么 结果的前 7 位就是什么。 (如果拿一个 1 去与另一个数相 & 就意味着我们要看那个数是多少) (2)取一个数的当中一段 就可以 `x & 0xFF` ![[image787.png]] 一个 int 有 32 比特 4字节,对 0xFF 进行取 & :前面 3 个字节都会变成 0,最后一个字节是什么 结果就会保留什么。 (给出多少个二进制的1 那些1对应的那些位会留下来 其他东西都拿走了) ### 按位取或( | ) ![[image788.png]] ![[image789.png]] #### 妙用 (1)使得某一位或某几位为1 可以用 `x | 0x01` ![[image790.png]] 希望该数最右边的那个比特为1 不管它原来是0还是1 或(|)上1后一定为1——与0或保留原样 与1或将该位变成1。 (2)把两个数拼起来:`0x00FF | 0xFF00` ![[image791.png]] ### 按位取反(~) ![[image792.png]] ![[image793.png]] ### 按位异或(^) ![[image794.png]] 两个位相等 结果为0 不相等 结果为1 ![[image795.png]] (1)对一个数做一次异或得到一个结果 再用该数对结果做异或 就翻回去了 x^y^y——>x 对一个变量同一个值异或俩次 等于什么都没做 (2)如果x、y相等 那么x^y的结果为0 ![[image796.png]] 它就可以用来做一个简单的加密 先全部异或得到乱七八糟的东西 再异或回来得到想要的 也可以用来判断两个数是否相同 还能写出这样的程序 交换两个数(不借助第三个变量): ```c void swap(int *x, int *y) { *y = *x ^ *y; *x = *x ^ *y; *y = *x ^ *y; } ``` 演算过程(*x、*y 的值一步步变化): ```text *y = *x ^ *y; a a^b *x = *x ^ *y; a^(a^b) a^b → *x 变成 b *y = *x ^ *y; b b^(a^b)=(b^b)^a=a → *y 变成 a ``` 再基于这个 swap 函数还能实现数组逆序: ```c void swap_array(int a[], int cnt) { int first, last; for (first=0, last=cnt-1; first 4 5&&4 —> 1 —> 1&1 —> 1 5|4 —> 5 5||4 —> 1 —> 1|1 —> 1 ~4 —> -5 !4 —> !1 —> ~1 —> 0 ``` 还有一个区别在于 逻辑运算中 如果第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值 ```text 1 || 任意 为 1 0 && 任意 为 0 ``` 可以使用位级和逻辑运算写出一个表达式 等价于x==y 即当x与y相等时 返回1否则返回0 两个相同的数做异或结果是0 可以用来判断相等——`x ^ y` 这样返回的是0 希望的是相等时得1,所以再来个 `!`:`!(x^y)` 这样就达到目的 相等得1 不相等得0 ### 移位运算 ### 左移 ![[image797.png]] ![[image798.png]] 因为结果是int 所以A5变成了294 移出去的10被看成了0010 它不只有八位 ![[image799.png]] 如果把结果当成十进制看 ![[image800.png]] 结果刚好是4倍的关系 移一位等价于乘2,移两位等价于乘4 至于能移多少位 取决于int有多大 (其实跟之前遇见的取一个十进制数的一位除以10一样 二进制移一位不就是乘除2嘛) (运算器做乘除运算就是通过移位来进行的) ### 右移 ![[image801.png]] ![[image802.png]] 因为右移会涉及到符号位 如果是有符号数移动 也补0的话 就从负变成正了 显然不合理 ![[image803.png]] 所以其实内部是这样的 对于无符号数右移(红色部分)左边补0 对于有符号数右移(蓝色)左边补与最高位相同的那个数 (首位为1 右移几位左边补几个1 首位为0 右移几位左边补几个0) ![[image804.png]] ![[image805.png]] 无符号的 b `0100……0000`: ![[image806.png]] ![[image807.png]] 有符号的 a `1100……0000`: ```text - 1000……0000 + 0100……0000 = -0100……0000 ``` 同样 用十进制看的话其实就是除了2 ![[image808.png]] ### 另外的 - 移位的时候不要用负数 这是没有定义的行为:`x << -2 // !!NO!!` - 移位运算是自左向右可结合的:`x << j << k` 等价于 `(x << j) << k` - 右移时左边补 0 的操作叫**逻辑右移**;左边补最高有效位的值的操作叫**算术右移** - c语言并没有明确定义对有符号数进行哪种右移 这可能出现可移植性的问题——但实际上所有编译器和机器组合都对有符号数进行算术右移 程序员也都假设会这样做;对于无符号数右移必须是逻辑的 其实就简单认为:**无符号数补0 有符号数补符号位**就行了。 在java中对于右移有着更明确的定义(这也是为什么要多此一举引出逻辑、算术右移概念的原因): ```java x >> k // 算术右移 x >>> k // 逻辑右移 ``` 优先级的问题:`1 << 2 + 3 << 4` 本意是 `(1<<2) + (3<<4)`,实际上是 `1 << (2+3) << 4`——**加减的优先级比移位高**,再加上自左向右结合,结果会是 `(1<<(2+3))<<4`。 所以 拿不准的时候 根据本意加上括号:`(1<<2) + (3<<4)` ![[image809.jpeg]] ### 一些运用 我们可以用 `%x` `%o` `%d` 输出16进制、8进制和10进制,但没有现成的东西帮我们输出2进制: ```c #include int main() { int number; scanf_s("%i", &number); // %i:能按前缀识别 8/10/16 进制 unsigned mask = 1u << 31; // 1u:unsigned 的 1;左移31位 = 0x8000……0000 for (; mask; mask >>= 1) // mask 从最高位逐位右移,32 轮后变全 0 跳出 { printf("%d", number & mask ? 1 : 0); } printf("\n"); return 0; } ``` ![[image810.png]] ![[image811.png]] 原理:循环每一轮拿 number 和 mask 取一下 `&`——结果非 0 就输出 1 否则输出 0,依次判断 number 的每一位上是什么 把它输出出来。 ![[image812.png]] ![[image813.png]] 我们可以使用 `%i` 去读取不同的进制数 使得该程序能将8、10、16都换成2进制: - 以 `0` 开头输入 表示输入的数为8进制 ![[image814.png]] - 十进制无需前缀 ![[image815.png]] - 以 `0x` 开头输入 表示输入的为16进制 ![[image816.png]] ## c++ ### 静态成员 **静态成员变量**:**类的属性**而非对象的属性——所有实例共享同一份。 - 用 `static` 声明,通常私有部分声明、**类外初始化**:`int MyClass::staticVar = 0;` - 常用于跟踪类的全局属性(如实例数) **静态成员函数**:不依赖实例,可用类名直接调用;**不能访问非静态成员**(没有对象上下文)。常用于与类相关的工具函数。 **访问方式**:类名(推荐,`MyClass::staticVar`)或对象(`obj.staticVar`,不推荐)。 ![[image817.png]] ![[image818.png]] ![[image819.png]] ![[image820.png]] ### 常数据成员 - **不能在类定义中直接初始化**——它是**对象特定**的:每个对象可以有不同的常数据成员值 - **必须在构造函数初始化列表中初始化**(常量一旦初始化不能再改) ```cpp class MyClass { public: const int constantValue; // 不能在类定义中直接初始化 MyClass(int value) : constantValue(value) { // 在构造函数初始化列表中初始化 } }; int main() { MyClass obj1(42); MyClass obj2(100); // obj1.constantValue = 42;obj2.constantValue = 100 } ``` 对比:静态数据成员与类关联(全类共享一份),可在类定义中考虑初始化;常数据成员与对象关联,必须逐对象在初始化列表中确定。 ![[image821.png]] ### 构造与析构 **构造函数**:与类同名、无返回类型、可重载(多个构造函数按参数列表自动选择);创建对象时**自动调用**。 - `MyClass obj;` → 构造函数自动调用 - `MyClass* ptr;` → **只定义指针、未创建对象,构造函数不调用** - `MyClass* ptr = new MyClass;` → 动态创建,构造函数调用 **析构函数**:对象销毁时自动调用,清理资源;`new` 出来的对象要 `delete` 触发析构: ```cpp MyClass* ptr = new MyClass; delete ptr; // 析构函数被自动调用 ``` ![[image822.png]] ![[image823.png]] ### 继承 #### 构造与析构 继承中的调用顺序(高频考点): - **构造**:先基类后派生类(基类构造函数初始化继承来的成员,派生类构造函数初始化自己的成员);基类有多个构造函数时,派生类在初始化列表中选择调用哪个 - **析构**:先派生类后基类(与构造相反) - **多层继承**:构造自底向上(Grandparent → Parent → Child),析构自顶向下(Child → Parent → Grandparent) 口诀:**构造像盖楼(先打地基),析构像拆楼(先拆顶层)**。 ![[image824.png]] ![[image825.png]] ![[image826.png]] ![[image827.png]] ![[image828.png]] #### 访问权限 public 继承下,基类成员在派生类中的可见性: - **public 成员** → 派生类中仍是 public,外部代码可访问 - **protected 成员** → 派生类中是 protected,只有派生类及其友元可访问,外部不可 - **private 成员** → 派生类中变为不可直接访问(只有基类自己的成员函数能访问) ![[image829.png]] #### 多继承二义性问题 两种解决方法: **1. 虚基类(Virtual Base Class)**:多个派生类继承同一基类、且这些派生类又有共同派生类时,用 `virtual` 声明虚基类,确保最终派生类中**只有一份基类实例**,消除二义性。 ```cpp class Base { /* ... */ }; class Derived : public virtual Base { /* ... */ }; // virtual 声明虚基类 ``` **2. 作用域分辨符 `::`**:同名成员二义性时,用 `类名::成员名` 明确指定用哪个。 ![[image830.png]] ```cpp #include class Base { public: void display() { std::cout << "Base class" << std::endl; } }; class Derived : public Base { public: void display() { std::cout << "Derived class" << std::endl; } void displayBoth() { Base::display(); // 指定调用基类的 Derived::display(); // 指定调用派生类的 } }; int main() { Derived d; d.displayBoth(); return 0; } ``` ![[image831.png]] ![[image832.png]] ![[image833.png]] #### 其他 ![[image834.png]] ![[image835.png]] ![[image836.png]] ![[image837.png]] ![[image838.png]] ### 多态(动态联编) 动态联编(Dynamic Binding)= 运行时多态:程序**运行时**决定调用哪个函数版本——用**基类指针或引用**指向派生类对象,调用**虚函数**时按对象的实际类型(而非指针类型)选择函数。 ![[image839.png]] ![[image840.png]] ![[image841.png]] ![[image842.png]] - **虚函数**:基类中用 `virtual` 声明,派生类可重写(override) - 基类指针/引用可引用派生类对象 → 不同派生类对象可放进同一容器、传给同一函数 ```cpp class Base { public: virtual void show() { std::cout << "Base class" << std::endl; } }; class Derived : public Base { public: void show() override { std::cout << "Derived class" << std::endl; } }; int main() { Base* ptr; Derived d; ptr = &d; // 基类指针指向派生类对象 ptr->show(); // 运行时选择 Derived::show() return 0; } ``` ![[image843.png]] ### 存储类 存储类 = 指定变量的**存储生命周期和作用域**的关键字: - **auto(自动)**:默认存储类。局部作用域内声明、仅作用域内可见,进入创建、退出销毁 - **register(寄存器)**:建议(非强制)编译器把变量放寄存器加速访问;**不能取地址** - **extern(外部)**:全局变量,程序所有文件可见(多文件共享);程序启动创建、结束销毁 - **static(静态)**:静态局部变量跨函数调用保持值;静态全局变量只初始化一次;静态成员变量被所有实例共享 - **thread_local(线程局部)**:每个线程有自己独立的副本,用于多线程编程 ![[image844.png]] ![[image845.png]] ![[image846.png]] ### 语句 ![[image847.png]] 后缀自增 `++` 只能用于**左值**(可被赋值的表达式)。`(x + y)` 的结果是临时值(右值),不可修改,所以 `(x + y)++` 非法;想递增就和应先存下来再 `sum++`。 ```cpp int x = 5; int y = 10; int sum = x + y; sum++; // 递增 sum 的值 ``` ### 读程序 ![[image848.png]] ![[image849.png]] ![[image850.png]] ![[image851.png]] ![[image852.png]] ### 其他 **零初始化规则**:全局变量和静态局部变量未显式初始化时,自动初始化为**零/默认值**(int → 0,double → 0.0,指针 → NULL)。 ```cpp int globalVar; // 自动初始化为0 double globalDouble; // 自动初始化为0.0 void myFunction() { static int staticLocalVar; // 自动初始化为0 } ``` ![[image853.png]] ![[image854.png]] ![[image855.png]] **C++ ≠ C**:C++ 扩展了 C(OOP、类和对象、函数重载、运算符重载、STL、异常处理、命名空间、虚函数与多态),并保持与 C 兼容,但它们是**两种不同的语言**,不能简单把 C++ 看作"带类的 C"。 ⚠️ 纠错:"变量的类型高低是指它被存放在内存的地址值大小"——**不准确**。类型高低与**数据类型**有关,与内存地址无关:数据类型决定存储空间大小和表示方式;地址由编译器分配、每次运行可能变化、与类型无关。 ![[image856.png]] **转换构造函数**:**单参数**构造函数具有类型转换作用——把参数类型隐式或显式转换为类类型。 ```cpp class MyString { public: MyString(const char* str) { // 将 const char* 转换为 MyString } }; int main() { const char* cString = "Hello, World!"; MyString myStr = cString; // 隐式调用转换构造函数 return 0; } ``` 小心使用:不必要的隐式转换可能导致意外行为,只用于有意义的转换、避免歧义。 ![[image857.png]] **原型声明 vs 简单声明**: - **原型声明**:放头文件(.h/.hpp),含完整接口信息、无函数体;可被多个源文件 include 共享(常配 `#ifndef/#define/#endif` 头文件守卫) - **简单声明**:源文件内声明函数,适合同一文件中定义并使用 ```cpp // add.h #ifndef ADD_H #define ADD_H int add(int a, int b); // 原型声明 #endif ``` ![[image858.png]] **作用域隐藏**:内层作用域声明与外层同名的标识符时,内层**隐藏**外层;内层作用域结束后外层标识符再次可见。 ```cpp #include int main() { int x = 5; // 外层 x std::cout << "Outer x: " << x << std::endl; { int x = 10; // 内层 x,隐藏外层 x std::cout << "Inner x: " << x << std::endl; } // 内层结束,内层 x 销毁 std::cout << "Outer x: " << x << std::endl; // 又是外层 x return 0; } ``` ![[image859.png]] ![[image860.png]] ![[image861.png]] ![[image862.png]] ⬅️ [[12-移动互联网应用|上一篇]] 🏠 [[00-信息基础大赛]] ➡️ [[00-信息基础大赛|回到总览]]