第 240~300 题:C、C++

📚 本文是 信息基础大赛(10.29 备赛)的第 13 篇,题目以截图为主、文字为点拨。

易错题集

image766 image767 image768 image769 image770 image771 image772 image773 image774 image775 image776 image777 image778

文件相关

使用<stdio.h>头文件中提供的函数。

文件指针:

在C中,文件操作需要使用文件指针来跟踪文件的位置和状态。FILE类型是用于表示文件指针的结构体类型。可以使用fopen()函数打开文件并获得文件指针,使用fclose()函数关闭文件。

FILE *file = fopen("example.txt", "r"); // 打开文件
// ...
fclose(file); // 关闭文件

文件打开模式:

打开文件时,需要指定打开文件的模式,常见的模式包括:

"r":只读

"w":写入(如果文件存在则截断,如果文件不存在则创建)

"a":附加到文件末尾(如果文件不存在则创建)

"b":二进制模式

可以通过在模式后添加"b"来进行二进制文件操作,例如:"rb"表示以二进制只读模式打开文件。

image779 image780

文件读取函数:

使用fgetc()逐字符读取文件。

使用fgets()逐行读取文件。

使用fread()以二进制方式读取数据。

int ch = fgetc(file);           // 逐字符读
char buffer[100];
fgets(buffer, sizeof(buffer), file); // 逐行读
image781

文件写入函数:

使用fputc()逐字符写入文件。

使用fputs()逐行写入文件。

使用fwrite()以二进制方式写入数据。

fputc('A', file);             // 逐字符写
fputs("Hello, World!", file); // 逐行写

格式化输入输出:

使用fprintf()和fscanf()可以进行格式化的输入和输出,类似于printf()和scanf(),但指定文件流。

int value = 42;
fprintf(file, "The value is: %d\n", value);
fscanf(file, "The value is: %d", &value);

检查文件末尾:feof() —— 已到末尾返回非零值(通常 1),否则返回 0。

if (feof(file)) {
    printf("End of file reached\n");
}
image782

错误处理:

使用perror()函数可以打印文件操作相关的错误信息。

使用ferror()函数可以检查文件操作是否出错。

if (ferror(file)) {
    perror("File operation error");
}

文件定位:fseek() 定位到指定位置,ftell() 获取当前文件指针位置。

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

位与布尔

位运算

c语言被称作最接近底层的语言 原因之一是它提供很多接近底层的操作

比如 它支持按位进行布尔运算

在c语言中

&表示按位的与

|表示按位的或

~表示按位取反

^表示按位的异或

<<表示左移

表示右移

确定一个位级表达式的结果的最好的方法 就是把16进制的参数拓展成二进制表示 并执行2进制运算 然后再转换回16进制

~0x41  —>  ~[0100 0001]  —>  [1011 1110]  —>  0xBE

按位与(&)

两个数x,y做与 在对应的位上面进行布尔运算

image784 image785

妙用

(1)如果希望让某一位或某些位为0

image786

就可以 x & 0xFE

FE 的最后一位是0 所以不管 x 的最后一位是什么 结果都是0;FE 前面的 7 位都是1 所以 x 的前 7 位是什么 结果的前 7 位就是什么。

(如果拿一个 1 去与另一个数相 & 就意味着我们要看那个数是多少)

(2)取一个数的当中一段

就可以 x & 0xFF

image787

一个 int 有 32 比特 4字节,对 0xFF 进行取 & :前面 3 个字节都会变成 0,最后一个字节是什么 结果就会保留什么。

(给出多少个二进制的1 那些1对应的那些位会留下来 其他东西都拿走了)

按位取或( | )

image788 image789

妙用

(1)使得某一位或某几位为1

可以用 x | 0x01

image790

希望该数最右边的那个比特为1 不管它原来是0还是1 或(|)上1后一定为1——与0或保留原样 与1或将该位变成1。

(2)把两个数拼起来:0x00FF | 0xFF00

image791

按位取反(~)

image792 image793

按位异或(^)

image794

两个位相等 结果为0 不相等 结果为1

image795

(1)对一个数做一次异或得到一个结果 再用该数对结果做异或 就翻回去了

x^y^y——>x 对一个变量同一个值异或俩次 等于什么都没做

(2)如果x、y相等 那么x^y的结果为0

image796

它就可以用来做一个简单的加密 先全部异或得到乱七八糟的东西 再异或回来得到想要的

也可以用来判断两个数是否相同

还能写出这样的程序 交换两个数(不借助第三个变量):

void swap(int *x, int *y)
{
    *y = *x ^ *y;
    *x = *x ^ *y;
    *y = *x ^ *y;
}

演算过程(*x、*y 的值一步步变化):

*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 函数还能实现数组逆序:

void swap_array(int a[], int cnt)
{
    int first, last;
    for (first=0, last=cnt-1; first<last; first++, last--)
        swap(&a[first], &a[last]);
}

如果使用了 first<=last 那么对于奇数个数组单元时 中间那个数做的就是自己与自己异或 结果为0 所以不加等号 奇数个时 中间那个数不做操作

逻辑运算?按位运算?

对于逻辑运算来说 它只看到整数有两个值 要么是0要么是1 所以非0的值都认为是1

可以理解为逻辑运算是把所有非0值都变成1,然后做按位运算

因为实际上在计算机里头 只有按位计算 c语言中的逻辑运算只是加以封装后的形式

5&4 —> 4        5&&4 —> 1 —> 1&1 —> 1
5|4 —> 5        5||4 —> 1 —> 1|1 —> 1
~4 —> -5        !4 —> !1 —> ~1 —> 0

还有一个区别在于

逻辑运算中 如果第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值

1 || 任意  为 1        0 && 任意  为 0

可以使用位级和逻辑运算写出一个表达式 等价于x==y

即当x与y相等时 返回1否则返回0

两个相同的数做异或结果是0 可以用来判断相等——x ^ y 这样返回的是0 希望的是相等时得1,所以再来个 !!(x^y)

这样就达到目的 相等得1 不相等得0

移位运算

左移

image797 image798

因为结果是int 所以A5变成了294

移出去的10被看成了0010 它不只有八位

image799

如果把结果当成十进制看

image800

结果刚好是4倍的关系 移一位等价于乘2,移两位等价于乘4

至于能移多少位 取决于int有多大

(其实跟之前遇见的取一个十进制数的一位除以10一样 二进制移一位不就是乘除2嘛)

(运算器做乘除运算就是通过移位来进行的)

右移

image801 image802

因为右移会涉及到符号位 如果是有符号数移动 也补0的话 就从负变成正了 显然不合理

image803

所以其实内部是这样的 对于无符号数右移(红色部分)左边补0

对于有符号数右移(蓝色)左边补与最高位相同的那个数

(首位为1 右移几位左边补几个1 首位为0 右移几位左边补几个0)

image804 image805

无符号的 b 0100……0000

image806 image807

有符号的 a 1100……0000

- 1000……0000
+ 0100……0000
= -0100……0000

同样 用十进制看的话其实就是除了2

image808

另外的

  • 移位的时候不要用负数 这是没有定义的行为:x << -2 // !!NO!!
  • 移位运算是自左向右可结合的:x << j << k 等价于 (x << j) << k
  • 右移时左边补 0 的操作叫逻辑右移;左边补最高有效位的值的操作叫算术右移
  • c语言并没有明确定义对有符号数进行哪种右移 这可能出现可移植性的问题——但实际上所有编译器和机器组合都对有符号数进行算术右移 程序员也都假设会这样做;对于无符号数右移必须是逻辑的

其实就简单认为:无符号数补0 有符号数补符号位就行了。

在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

一些运用

我们可以用 %x %o %d 输出16进制、8进制和10进制,但没有现成的东西帮我们输出2进制:

#include <stdio.h>

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 image811

原理:循环每一轮拿 number 和 mask 取一下 &——结果非 0 就输出 1 否则输出 0,依次判断 number 的每一位上是什么 把它输出出来。

image812 image813

我们可以使用 %i 去读取不同的进制数 使得该程序能将8、10、16都换成2进制:

  • 0 开头输入 表示输入的数为8进制
image814
  • 十进制无需前缀
image815
  • 0x 开头输入 表示输入的为16进制
image816

c++

静态成员

静态成员变量类的属性而非对象的属性——所有实例共享同一份。

  • static 声明,通常私有部分声明、类外初始化int MyClass::staticVar = 0;
  • 常用于跟踪类的全局属性(如实例数)

静态成员函数:不依赖实例,可用类名直接调用;不能访问非静态成员(没有对象上下文)。常用于与类相关的工具函数。

访问方式:类名(推荐,MyClass::staticVar)或对象(obj.staticVar,不推荐)。

image817 image818 image819 image820

常数据成员

  • 不能在类定义中直接初始化——它是对象特定的:每个对象可以有不同的常数据成员值
  • 必须在构造函数初始化列表中初始化(常量一旦初始化不能再改)
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

构造与析构

构造函数:与类同名、无返回类型、可重载(多个构造函数按参数列表自动选择);创建对象时自动调用

  • MyClass obj; → 构造函数自动调用
  • MyClass* ptr;只定义指针、未创建对象,构造函数不调用
  • MyClass* ptr = new MyClass; → 动态创建,构造函数调用

析构函数:对象销毁时自动调用,清理资源;new 出来的对象要 delete 触发析构:

MyClass* ptr = new MyClass;
delete ptr; // 析构函数被自动调用
image822 image823

继承

构造与析构

继承中的调用顺序(高频考点):

  • 构造:先基类后派生类(基类构造函数初始化继承来的成员,派生类构造函数初始化自己的成员);基类有多个构造函数时,派生类在初始化列表中选择调用哪个
  • 析构:先派生类后基类(与构造相反)
  • 多层继承:构造自底向上(Grandparent → Parent → Child),析构自顶向下(Child → Parent → Grandparent)

口诀:构造像盖楼(先打地基),析构像拆楼(先拆顶层)

image824 image825 image826 image827 image828

访问权限

public 继承下,基类成员在派生类中的可见性:

  • public 成员 → 派生类中仍是 public,外部代码可访问
  • protected 成员 → 派生类中是 protected,只有派生类及其友元可访问,外部不可
  • private 成员 → 派生类中变为不可直接访问(只有基类自己的成员函数能访问)
image829

多继承二义性问题

两种解决方法:

1. 虚基类(Virtual Base Class):多个派生类继承同一基类、且这些派生类又有共同派生类时,用 virtual 声明虚基类,确保最终派生类中只有一份基类实例,消除二义性。

class Base { /* ... */ };

class Derived : public virtual Base { /* ... */ }; // virtual 声明虚基类

2. 作用域分辨符 :::同名成员二义性时,用 类名::成员名 明确指定用哪个。

image830
#include <iostream>

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 image832 image833

其他

image834 image835 image836 image837 image838

多态(动态联编)

动态联编(Dynamic Binding)= 运行时多态:程序运行时决定调用哪个函数版本——用基类指针或引用指向派生类对象,调用虚函数时按对象的实际类型(而非指针类型)选择函数。

image839 image840 image841 image842
  • 虚函数:基类中用 virtual 声明,派生类可重写(override)
  • 基类指针/引用可引用派生类对象 → 不同派生类对象可放进同一容器、传给同一函数
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

存储类

存储类 = 指定变量的存储生命周期和作用域的关键字:

  • auto(自动):默认存储类。局部作用域内声明、仅作用域内可见,进入创建、退出销毁
  • register(寄存器):建议(非强制)编译器把变量放寄存器加速访问;不能取地址
  • extern(外部):全局变量,程序所有文件可见(多文件共享);程序启动创建、结束销毁
  • static(静态):静态局部变量跨函数调用保持值;静态全局变量只初始化一次;静态成员变量被所有实例共享
  • thread_local(线程局部):每个线程有自己独立的副本,用于多线程编程
image844 image845 image846

语句

image847

后缀自增 ++ 只能用于左值(可被赋值的表达式)。(x + y) 的结果是临时值(右值),不可修改,所以 (x + y)++ 非法;想递增就和应先存下来再 sum++

int x = 5;
int y = 10;
int sum = x + y;
sum++; // 递增 sum 的值

读程序

image848 image849 image850 image851 image852

其他

零初始化规则:全局变量和静态局部变量未显式初始化时,自动初始化为零/默认值(int → 0,double → 0.0,指针 → NULL)。

int globalVar;        // 自动初始化为0
double globalDouble;  // 自动初始化为0.0

void myFunction() {
    static int staticLocalVar; // 自动初始化为0
}
image853 image854 image855

C++ ≠ C:C++ 扩展了 C(OOP、类和对象、函数重载、运算符重载、STL、异常处理、命名空间、虚函数与多态),并保持与 C 兼容,但它们是两种不同的语言,不能简单把 C++ 看作"带类的 C"。

⚠️ 纠错:"变量的类型高低是指它被存放在内存的地址值大小"——不准确。类型高低与数据类型有关,与内存地址无关:数据类型决定存储空间大小和表示方式;地址由编译器分配、每次运行可能变化、与类型无关。

image856

转换构造函数单参数构造函数具有类型转换作用——把参数类型隐式或显式转换为类类型。

class MyString {
public:
    MyString(const char* str) {
        // 将 const char* 转换为 MyString
    }
};

int main() {
    const char* cString = "Hello, World!";
    MyString myStr = cString; // 隐式调用转换构造函数
    return 0;
}

小心使用:不必要的隐式转换可能导致意外行为,只用于有意义的转换、避免歧义。

image857

原型声明 vs 简单声明

  • 原型声明:放头文件(.h/.hpp),含完整接口信息、无函数体;可被多个源文件 include 共享(常配 #ifndef/#define/#endif 头文件守卫)
  • 简单声明:源文件内声明函数,适合同一文件中定义并使用
// add.h
#ifndef ADD_H
#define ADD_H

int add(int a, int b); // 原型声明

#endif
image858

作用域隐藏:内层作用域声明与外层同名的标识符时,内层隐藏外层;内层作用域结束后外层标识符再次可见。

#include <iostream>

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 image860 image861 image862

⬅️ 上一篇 🏠 00-信息基础大赛 ➡️ 回到总览