sizeof 和 strlen 的区别?
1.1. sizeof 操作符
-
功能:
sizeof是一个 编译时操作符,用于获取数据类型或变量的大小,单位是字节(byte)。它计算的是 对象在内存中占据的总字节数。 -
用途:可以用来获取数据类型、数组、结构体或类等的大小。
-
注意:
sizeof操作符是在 编译时 求值的,不会在运行时产生开销。
int a = 10;
std::cout << sizeof(a) << std::endl; // 输出 4,假设 int 是 4 字节
int arr[10];
std::cout << sizeof(arr) << std::endl; // 输出 40,假设每个 int 占 4 字节,数组 10 个元素
char str[] = "Hello";
std::cout << sizeof(str) << std::endl; // 输出 6,因为 "Hello" 包含 5 个字符 + 1 个空字符(\0)
1.2. strlen 函数
-
功能:
strlen是一个 运行时函数,用于计算 C 风格字符串(以'\0'结尾的字符数组)的 长度,即字符串中 不包括终止的空字符(\0) 的字符数。 -
用途:只能用于 C 风格字符串(
char*或char[]),不能用于其他类型的数组或数据结构。 -
注意:
strlen会遍历字符串直到遇到\0,因此它的计算过程是 线性时间复杂度 O(n),其中n是字符串的长度。 -
char str[] = "Hello";
-
std::cout << strlen(str) << std::endl; // 输出 5,不包括 '\0'
1.3. 关键区别
| 特性 | sizeof | strlen |
|---|---|---|
| 类型 | 编译时操作符 | 运行时函数 |
| 适用类型 | 适用于任何类型(包括数组、指针、数据类型等) | 仅适用于 C 风格字符串(char* 或 char[]) |
| 返回值 | 返回对象在内存中的 大小(字节数) | 返回字符串中字符的 个数,不包括 \0 |
| 计算开销 | 无开销,因为是编译时求值 | 有开销,因为需要遍历字符串直到 \0 |
| 数组区别 | 对于数组,返回的是 整个数组的大小,包括所有元素 | 对于数组,返回的是字符串的 实际字符数(不含 \0) |
char str[] = "Hello"; // 长度 6 ('H' 'e' 'l' 'l' 'o' '\0')
std::cout << sizeof(str) << std::endl; // 输出 6,数组总大小
std::cout << strlen(str) << std::endl; // 输出 5,实际字符数
1.4. 额外注意点
-
对于指针:如果你使用
sizeof操作符来求一个指针类型的大小,它将返回指针本身所占的内存大小(通常是 4 字节或 8 字节,取决于平台),而不是它指向的数据的大小。
char *ptr = "Hello";
std::cout << sizeof(ptr) << std::endl; // 输出 8 或 4,取决于指针大小(平台相关)
std::cout << strlen(ptr) << std::endl; // 输出 5,计算字符串的长度(不包括 '\0')
2 一个指针可以是 volatile 吗?
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。 说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
3、设置地址为 0x65a9 的整型变量的值为 0xaa56
int *ptr;
ptr=(int *)0x65a9;
*ptr = 0xa56;
4、面向对象的三大特征
面向对象的三大特征是封装性、继承性和多态性: 封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection(private, protected, public)。 继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。 多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。 这部分需要熟悉掌握原理虚函数,了解一些概念(静态多态、动态多态)等。
5、对拷贝构造函数和赋值运算符的区别
✔️ 一句话总结
-
拷贝构造函数:用一个已有对象 创建 一个新的对象。
-
赋值运算符 operator=:把一个已有对象的值 赋给另外一个已经存在的对象。
📌 详细区别
5.1. 触发时机不同
拷贝构造函数在以下情况调用:
A b = a; // 用 a 创建 b
A b(a); // 同上
A func(); // 返回对象(某些情况会触发)
void foo(A a); // 函数按值传递参数
return a; // 返回局部对象
👉 特点:一定是在构造阶段发生,新对象被创建。
赋值运算符在以下情况调用:
A a;
A b;
a = b; // 给已经存在的对象“赋值”
5.2. 语法定义方式不同
拷贝构造函数
A(const A& other);
赋值运算符
A& operator=(const A& other);
5.3. 默认实现行为不同
-
默认 拷贝构造:把每个成员逐个拷贝。
-
默认 赋值运算符:也是成员逐个赋值。
但:
如果类中有资源(如 new 出来的内存、文件句柄、网络句柄),你就必须自己实现这两个函数,否则会发生:
-
浅拷贝问题(double free)
-
内存泄漏
5.4. 调用次数的语义差别
| 场景 | 调用哪个 |
|---|---|
| A b = a; | 拷贝构造 |
| A b(a); | 拷贝构造 |
| A b; b = a; | 赋值运算符 |
| 函数参数按值传递 | 拷贝构造 |
| 函数返回对象 | 拷贝构造(有优化) |
5.5. 内部实现逻辑也不同
拷贝构造函数一般写法
A::A(const A& other)
{
this->size = other.size;
this->data = new int[size];
memcpy(this->data, other.data, size * sizeof(int));
}
赋值运算符一般写法(注意自赋值检测)
A& A::operator=(const A& other)
{
if (this == &other)
return *this; // 防止自赋值
delete[] data; // 清理旧资源
size = other.size;
data = new int[size];
memcpy(data, other.data, size * sizeof(int));
return *this;
}
👉 区别在于赋值运算符要处理旧资源,而拷贝构造不需要
因为拷贝构造时对象是“空的”,尚未有旧资源。
⭐ 最容易混淆的例子
A a;
A b = a; // 拷贝构造
A c;
c = a; // 赋值运算符
🎯 记忆技巧
-
“= 号”出现在对象刚创建那一行 → 拷贝构造
-
“= 号”出现在对象声明之后 → 赋值运算符
6 一个指针可以是 volatile 吗?
✔️ 一句话总结
C++ 多态是通过 虚函数表(vtable) + 虚指针(vptr) 实现的:
-
每个含虚函数的类都有一个 vtable(虚函数指针数组)
-
每个对象内部都有一个隐藏的 vptr(指向 vtable)
-
运行时通过 vptr 查 vtable,从而找到真正要调用的派生类函数,实现“晚绑定”。
🔍 多态必须满足三个条件
-
基类函数必须是 virtual
-
用 基类指针(或引用)指向派生类对象
-
调用的是 虚函数
🚀 原理详解(非常简洁)
1. 类结构(含虚函数的类)
编译器会为 class 生成:
-
一个 虚函数表 vtable:
一个数组,每个元素是一个函数指针(虚函数地址)。
class Base {
public:
virtual void foo();
virtual void bar();
};
编译器生成:
Base::vtable = {
&Base::foo,
&Base::bar
}
2. 对象结构(含隐藏的 vptr)
每个对象内部都有一个隐藏成员:
+------------------------+
| vptr ——> Base::vtable |
+------------------------+
| 基类成员 |
+------------------------+
3. 派生类覆盖虚函数后,vtable 会被替换
class Derived : public Base {
public:
void foo() override;
};
Derived 也有自己的 vtable:
Derived::vtable = {
&Derived::foo, // 覆盖的函数
&Base::bar // 未覆盖的仍用基类的版本
}
对象结构:
Derived obj:
+------------------------------+
| vptr ——> Derived::vtable |
+------------------------------+
| Base成员 + Derived成员 |
+------------------------------+
4. 调用虚函数时发生什么?
Base* p = new Derived();
p->foo();
执行流程(真正的多态原理):
-
p 是 Base* 指针,但内部 vptr 指向 Derived::vtable
-
调用虚函数时:
-
编译器生成:
p->vptr[foo_index]()
-
-
foo_index 对应 foo 的槽位
-
vptr 指向的是 Derived::vtable
-
最终调用的是
Derived::foo() -
实现“基类指针调用派生类函数”
🧠 核心一句话记忆
多态 = 对象里藏着一个指向虚函数表的指针,调用虚函数时根据这个指针去查实际函数地址。
📌 多态为什么要虚函数?
因为需要 运行时动态绑定(late binding)。
普通函数调用是:
-
编译期就确定要调用哪个函数(静态绑定)
虚函数调用是:
-
运行时根据 vptr 动态决定调用哪个类的函数(动态绑定)
🎯 多态的底层代价
-
每个对象多一个指针(vptr)
-
每次虚函数调用多一次 vptr 查表
-
不能 inline(除非 devirtualization 优化成功)
✔️ 一个类有几个虚表(vtable)?
规则:一个类只有 一个虚表,除非发生“多重继承”。
| 情况 | 虚表数量 |
|---|---|
| 普通类,有虚函数 | 1 个 vtable |
| 单继承,派生类覆盖/新增虚函数 | 1 个 vtable(覆盖或扩展) |
| 多重继承(如:class C : public A, public B) | 有几个基类,就有几个 vtable |
| 虚继承 | 仍然可能有多个(由编译器实现决定),但通常 ≥1 |
✔️ 一个对象有几个虚函数指针(vptr)?
规则:一个对象里有几个基类的 vtable,就有几个 vptr。
| 情况 | 对象中 vptr 数量 |
|---|---|
| 普通含虚函数类对象 | 1 个 vptr |
| 单继承派生类对象 | 1 个 vptr |
| 多重继承对象 | 至少 2 个 vptr(对应每个有虚函数的基类) |
| 菱形继承 + virtual inheritance | vptr 更多,由编译器决定 |
✔️ 为什么多重继承会导致多个虚表?
class A { virtual void foo(); };
class B { virtual void bar(); };
class C : public A, public B { };
内存布局类似:
C 对象:
+------------------------+
| A 子对象的 vptr ——> A 的 vtable 或 C 的改写表 |
+------------------------+
| A 成员 |
+------------------------+
| B 子对象的 vptr ——> B 的 vtable 或 C 的改写表 |
+------------------------+
| B 成员 |
+------------------------+
| C 自己的成员 |
+------------------------+
因此 C 对象里 有两个 vptr,每个 vptr 指向各自的虚表。
1万+

被折叠的 条评论
为什么被折叠?



