常用关键字考察
- static
- inline
- define
- [struct 和class的区别 (重点)](#struct 和class的区别 (重点))
- C语言和C++有什么区别
- const
- mutable
- explicit
- override
- final
- strlen和sizeof
- constexpr
- typeid
- [nullptr 重点](#nullptr 重点)
static
静态成员变量:
- 不考虑累的情况
隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
cpp
// 假设这是 File_A.cpp
#include <iostream>
// 1. 【隐藏性】
// 这个变量只能在 File_A.cpp 中被看见。
// 如果在 File_B.cpp 中尝试用 extern int globalVar; 访问它,会报错(链接错误)。
static int global_val; // 2. 【默认初始化】未手动赋值,编译器自动将其置为 0
int main() {
std::cout << "Global Static Value: " << global_val << std::endl;
// 输出: 0
return 0;
}
默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区,静态成员变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。
cpp
#include <iostream>
void counterFunction() {
// 【记忆性】与【只初始化一次】
// 下面这行代码只在第一次调用函数时执行。
// 第二次调用时,会直接跳过初始化,复用上一次的值。
static int count = 0;
// 普通变量(对比用):每次调用都会重新创建并销毁
int temp = 0;
count++;
temp++;
std::cout << "Static count: " << count << " | Normal temp: " << temp << std::endl;
}
int main() {
std::cout << "--- 第1次调用 ---" << std::endl;
counterFunction();
// 输出: Static count: 1 | Normal temp: 1
std::cout << "--- 第2次调用 ---" << std::endl;
counterFunction();
// 输出: Static count: 2 | Normal temp: 1 <-- 看到区别了吗?count 记住了上次的值
// std::cout << count; // 【作用域】报错!虽然 count 还活着,但在 main 函数里看不见它。
return 0;
}
第二点考虑类的情况:
类的静态成员变量只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类外定义外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问
cpp
#include <iostream>
class Player {
public:
int id; // 普通成员变量:每个对象独有一份
// 1. 【声明】
// 只是告诉编译器有这个东西,但没有分配内存。
// 不能在这里赋值(除非是 static const 且为整型,那是特例)。
static int total_players;
Player(int i) : id(i) {
// 4. 【访问】
// 构造函数是非 static 成员函数,它可以任意访问 static 成员变量
total_players++;
}
void showInfo() {
std::cout << "Player ID: " << id << ", Total Players: " << total_players << std::endl;
}
};
// 2. 【定义与初始化】
// 必须在类外!必须在全局作用域!
// 这里分配了内存空间。注意:这里不需要再写 static 关键字。
int Player::total_players = 0;
int main() {
// 3. 【与类关联,不与对象关联】
// 还没创建对象,就可以直接通过类名访问
std::cout << "Start count: " << Player::total_players << std::endl;
Player p1(101);
Player p2(102);
// p1 和 p2 共享同一个 total_players
p1.showInfo(); // 输出 Total: 2
p2.showInfo(); // 输出 Total: 2
// 如果修改 p1 看到的静态变量,p2 的也会变
p1.total_players = 0;
std::cout << "After reset by p1, p2 sees: " << p2.total_players << std::endl; // 输出 0
return 0;
}
静态函数是否可以调用非静态成员函数
○ 在C++中,静态成员函数可以调用非静态成员函数,但需要通过对象或对象指针来调用。
非静态成员函数是否可以调用静态成员函数
○ 是的,C++中的非静态成员函数可以调用静态成员函数。
○ 非静态成员函数可以通过类名或类的对象来访问和调用静态成员函数。静态成员函数不依赖于类的对象,因此可以在非静态成员函数中直接使用类名或对象来调用静态成员函数。
静态成员函数是否可以调用类的构造函数
能。 静态成员函数虽然没有 this 指针, 不能访问具体实例的非静态成员 ,但它具有类域的访问权限 。它不仅能调用构造函数,甚至常用于调用私有构造函数来实现工厂模式或单例模式,控制对象的生成逻辑。
static成员函数是否可以被virtual修饰
static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function。
静态成员存储在哪里?
普通成员变量存储在栈或堆中,而静态成员变量存储在静态全局区,程序启动时分配,程序结束时释放。它与类的对象何时创建、何时销毁完全无关。
全局变量和static变量的区别
- 无论是 Static 函数还是普通函数,它们的二进制指令在内存的代码段中都只有一份 。并不存在'普通函数每次调用拷贝一份代码'的情况,每次调用产生的是新的栈帧,用于存放局部变量
- Static 函数具有内部链接性,它只在当前编译单元(.cpp文件)内可见 ,不会污染全局符号表,避免了命名冲突。
- 普通函数默认具有外部链接性,可以被其他源文件通过 extern 调用。
- 对于变量的存储,局部变量存在于栈(Stack) ,全局/Static变量存在于静态全局区 ,动态申请的数据存在于堆(Heap)
面试追问:既然 static 可以限制作用域,那我在头文件(.h)里定义 static 变量好不好?"
不可以
如果如果在 header.h 里写 static int count = 0;,然后 A.cpp 和 B.cpp 都 include 了它
结果是A.cpp 有了一个私有的 count,B.cpp 也有了一个私有的 count
它们是两个完全不同的内存地址! 我在 A 里修改了 count,B 里的 count 根本不会变。
C++17 之前:在 .h 用 extern 声明,在 .cpp 定义。
C++17 之后:直接在 .h 使用 inline 变量(inline int count = 0;),保证全局唯一且只定义一次。"
inline
- inline 是一种以空间换时间的优化机制 。它建议编译器在编译阶段 ,将函数体直接嵌入到调用处 ,从而消除了函数栈帧的创建与销毁开销,提升执行效率
- 与宏的区别(关键点):相比 C 语言的宏定义,inline 是真正的函数。它在编译期通过了类型检查,且保留了函数的参数求值顺序 ,避免了宏定义中常见的'二次副作用'陷阱 ,因此比宏更安全、更易调试
下面这些代码可以看出来宏只是单纯的文本替换
cpp
// 宏定义:简单的文本替换
#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
// 内联函数:真正的函数调用语义
inline int max_inline(int a, int b) {
return a > b ? a : b;
}
int main() {
int x = 5;
int y = 0;
// 场景:宏的陷阱
// 宏展开后变成: ((++x) > (y) ? (++x) : (y))
// x 被加了两次!结果 x 变成 7
int res1 = MAX_MACRO(++x, y);
x = 5; // 重置
// 场景:内联函数的安全性
// 先计算参数 ++x (变成6),再传给函数
// 结果 x 变成 6
int res2 = max_inline(++x, y);
return 0;
}
- 编译器机制: inline 关键字对编译器仅仅是一个建议,而非强制命令 。如果函数体过大、包含复杂的控制流程 (如 while/switch)或递归调用,编译器会忽略 inline,将其作为普通函数处理。"
- 类与构造函数的陷阱(高分点):虽然在类内部定义的成员函数默认是 inline 的,但不建议手动将复杂的构造和析构函数声明为 inline
- 因为编译器会在其中隐式插入大量初始化代码 (如基类构造、成员对象构造、vptr 初始化)。若强制内联,会导致代码体积剧烈膨胀(Code Bloat),增加指令缓存(Instruction Cache)的未命中率,反而降低性能。
define
宏定义只是在预处理阶段简单的进行 文本替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然容易出错
struct 和class的区别 (重点)
struct默认是公有的,class默认是私有的;class默认是private继承,struct默认是public继承
并且c++的struct和c的struct相比,还多了个支持函数定义的功能
在工程上struct:通常用于定义一个数据包,不包含复杂功能
class:用于定义具有复杂行为的对象,包含私有数据和操作接口
C语言和C++有什么区别
"核心在于设计理念。C 是面向过程的,侧重于底层控制和性能;C++ 是多范式的,引入了面向对象(封装、继承、多态)和泛型编程(模板)。
具体体现在:C++ 提供了 RAII 内存管理机制(new/delete),更安全的类型检查,以及强大的 STL 标准库,极大地提高了开发效率和代码复用性。
const
针对变量
普通变量:(1)常变量,值不可变;(2)定义const变量时,需要对它进行初始化;(3)如果类的成员变量是const类型的变量,那么该变量不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数
指针:"左定值,右定址"
以星号 * 为界:
const 在 * 的左边:修饰的是值(内容不能改)。
const 在 * 的右边:修饰的是指针(地址不能改)。
针对函数:
类的成员函数,若指定为const类型,表明是常函数,不能修改类的成员变量,并且类的常对象只能访问类的常成员函数
mutable
可变的,易变的;在c++中,mutable也是为了突破const的限制而设置的;在const函数里面修改一些跟类状态无关的数据成员,那么这个变量就需要被mutable修饰
mutable是用来修饰变量的,被修饰的变量,将永远处于可变的状态;1.在常函数里,被修饰的变量可以被修改;2.在常对象中也可以被修改
explicit
explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换
cpp
#include <iostream>
class MyBuffer {
public:
int size;
// 1. 没有 explicit:允许隐式转换
// 这是一个危险的构造函数,因为 int 可以被误解
MyBuffer(int s) : size(s) {
std::cout << "Allocating buffer of size " << size << std::endl;
}
};
void processBuffer(const MyBuffer& buf) {
// 处理业务...
}
int main() {
// === 正常用法 ===
MyBuffer b1(1024); // OK,很清晰
// === 隐式转换的陷阱 ===
// 程序员的本意:可能是想把字符 'a' 传进去处理
// 编译器的行为:'a' 是 char,char 也是整数(ASCII码 97)
// 于是编译器偷偷构造了一个 MyBuffer(97)!
processBuffer('a');
// 结果:你以为你在传字符,实际上你分配了 97 个字节的内存!
// 这就是隐式转换带来的逻辑灾难。
return 0;
}
cpp
class MyBufferSafe {
public:
// 2. 加上 explicit:拒绝隐式转换
explicit MyBufferSafe(int s) : size(s) {}
};
void processSafe(const MyBufferSafe& buf) {}
int main() {
// processSafe(100); // ❌ 编译报错!编译器说:不能把 int 隐式转为 Object
processSafe(MyBufferSafe(100)); // ✅ 正确:显式调用,意图明确
return 0;
}
explicit用来修饰类的构造函数,只能用于类内部的构造函数声明上,用于单个参数的构造函数,且不能发生相应的隐式类型转换,只能以显示的方式进行类型转换
override
指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的。
如果没有 override,当你写错参数或漏掉 const 时,编译器会以为你想定义一个新的函数,从而导致多态静默失效
clike
class Base {
public:
// 基类:接受 int 参数
virtual void move(int step) const {}
};
class Derived : public Base {
public:
// ❌ 陷阱:程序员本来想重写,但手滑把 int 写成了 float// 【没有 override】// 编译器:OK,你定义了一个新函数 move(float),这与基类无关。// 结果:多态失效,且没有任何报错。
void move(float step) const {}
// ✅ 救星:加上 override// 【有 override// 编译器:等等,你说你要重写基类函数?// 我去查了,基类里没有 move(float)!// 报错:'move' marked 'override' but does not override any member functions
void move(float step) const override {}
};
final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错
clike
// 1. 修饰类:我就是最终形态,谁也别想继承我
class SuperBase final {
// ...
};
// ❌ 编译报错:cannot derive from 'final' base 'SuperBase'
class TryToInherit : public SuperBase {
};
clike
class Base {
public:
virtual void attack() { /*...*/ }
};
class Intermediate : public Base {
public:
// 2. 修饰函数:这个 attack 逻辑我定死了,子类不准改!
void attack() final {
// 关键逻辑...
}
};
class Derived : public Intermediate {
public:
// ❌ 编译报错:virtual function 'attack' overriding final function
void attack() override {
}
};
strlen和sizeof
sizeof是运算符,strlen是字符处理函数
sizeof参数是任意的数据类型或者数据;strlen的参数只能是字符串指针且结尾是'\0'的字符串
如果参数是指针的话,sizeof计算的是指针的长度,而不是对应数据或者字符串的长度;
constexpr
constexpr出现是为了解决const的双重语义:只读变量和修饰常量
C++11标准为了解决const关键字的双重语义问题,保留了const表示"只读"的语义,而将"常量"的语义划分给了新添加的constexpr关键字
所以,C++11 标准中,建议将const和constexpr的功能区分开,表达"只读"语义的场景用const,表达"常量"语义的场景用constexpr
- 当然,constexpr修饰的函数也有一定的限制:(1)函数体尽量只包含一个return语句,多个可能会编译出错;(2)函数体可以包含其他语句,但是不能是运行期语句,只能是编译期语句;编译器会将constexpr函数视为内联函数!所以在编译时若能求出其值,则会把函数调用替换成结果值。
- constexpr还能修饰类的构造函数,即保证传递给该构造函数的所有参数都是constexpr,那么产生的对象的所有成员都是constexpr。该对象是constexpr对象了,可用于只使用constexpr的场合。 注意constexpr构造函数的函数体必须为空,所有成员变量的初始化都放到初始化列表中。
clike
#include <iostream>
int main() {
int n;
std::cin >> n; // 假设用户输入 10
// === const 的包容 ===// ✅ 正确:const 可以在运行时初始化。// 虽然是常量,但它的值直到运行到这一行才知道。
const int a = n;
// === constexpr 的苛刻 ===// ❌ 编译报错!// 编译器怒吼:n 是运行时的值,我编译的时候怎么知道它是多少?// constexpr int b = n;
// ✅ 正确:1 + 2 是字面量,编译器一眼就能算出是 3。
constexpr int c = 1 + 2;
return 0;
}
typeid
获取一个实例的类型
当typeid操作符的操作数是不带有虚函数的类类型时,typeid操作符会指出操作数的类型,而不是底层对象的类型
如果typeid操作符的操作数是至少包含一个虚拟函数的类类型时,并且该表达式是一个基类的引用,则typeid操作符指出底层对象的派生类类型
clike
#include <iostream>
#include <typeinfo>
// 1. 无虚函数的基类
class NoVirtualBase { };
class NoVirtualDerived : public NoVirtualBase { };
// 2. 有虚函数的基类 (多态)
class PolyBase { virtual void func() {} };
class PolyDerived : public PolyBase { };
int main() {
// === 场景 1:非多态 (静态决议) ===
NoVirtualDerived nd;
NoVirtualBase* ptr1 = &nd;
// 陷阱:虽然 ptr1 指向的是 Derived,但因为它没有虚函数
// 编译器偷懒了,直接看 ptr1 的定义类型是 Base*
std::cout << typeid(*ptr1).name() << std::endl;
// 输出:class NoVirtualBase (它看不透真相)
// === 场景 2:多态 (动态决议) ===
PolyDerived pd;
PolyBase* ptr2 = &pd;
// 正解:因为有虚函数,typeid 会去查 vptr
std::cout << typeid(*ptr2).name() << std::endl;
// 输出:class PolyDerived (成功还原真相!)
// === 场景 3:指针本身的类型 ===
// 注意:typeid(ptr2) 测的是指针变量本身,不是它指向的对象
std::cout << typeid(ptr2).name() << std::endl;
// 输出:class PolyBase* (指针本身当然是基类指针)
return 0;
}
nullptr 重点
在 C++ 中,NULL 本质上是一个宏,通常定义为整数 0。在函数重载时,如果参数分别是 int 和 pointer,传入 NULL 会被优先匹配为整数,导致逻辑错误。而 nullptr 是强类型的,只能匹配指针版本
"nullptr 的类型是 std::nullptr_t,它可以隐式转换为任何指针类型,但绝不转换为整数,这让代码更加安全清晰。"