IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++虚函数与多态实现 -> 正文阅读

[C++知识库]C++虚函数与多态实现

虚函数实现与c语言中的回调函数

虚函数的发明初衷在于使得程序具有更好的通用性,从代码编写的角度看,其使用基类的指针指向子类的对象,并调用虚函数,使得根据指向的对象 而调用不同的子类函数,从而实现运行时多态(Runtime polymorphism),这个特性与c语言中的函数指针回调非常相似,在c语言中,我们将函数指针作为参数传递给其他函数,然后调用,根据其指向的不同函数而产生不同的行为,c代码示例如下

#include <stdio.h>

typedef void(*func)(void);

void foo() {
    printf("foo() called\n");
}

void bar() {
    printf("bar() called\n");
}

void callback(void* fn) {
    ((func)fn)();
}

int main() {
  callback((void*)foo);
  callback((void*)bar);
  return 0;
}

可见,callback()函数根据传入的函数指针的不同使得其调用的函数不同,因此对于callback函数来说,其发生了运行时多态(Runtime polymorphism),从汇编的角度看,其对应的汇编代码可以是

## call
call *%rdi <fn>

其中 %rdi 寄存器保存函数的第一个参数,*%rdi表示寄存器存的是调用地址,此行汇编执行完后,栈空间将返回地址压入栈中,程序计数寄存器 pc的值将保存为%rdi 里的内容,控制转移至调用的函数中。

下面来看C++中虚函数是如何实现多态的,先给出一个使用virtual关键字的多态实现

#include <iostream>

using std::cout;
using std::endl;

class A {
    public :
        virtual void foo() {
            cout << "A::foo() Virtual Table address : "
                 <<(void*)(*reinterpret_cast<int*>(this)) << endl;
        }

        virtual ~A() {
            cout << "~A() Virtual Table address : "
                 <<(void*)(*reinterpret_cast<int*>(this)) << endl;
        }
};

class B : public A {
    public :
        void foo() {
            cout << "B::foo() Virtual Table address : "
                 <<(void*)(*reinterpret_cast<int*>(this)) << endl;
        }

        ~B() {
            cout << "~B() Virtual Table address : "
                 <<(void*)(*reinterpret_cast<int*>(this)) << endl;
        }
};

int main() {
    A* p = new B();
    p->foo();
    delete p;
    return 0;
}
===========================================
output:
B::foo() Virtual Table address : 0x1fe2048
~B() Virtual Table address : 0x1fe2048
~A() Virtual Table address : 0x1fe2098

可以看到当析构函数定义为虚函数时,先执行子类B的析构函数,可以看到此时对象首地址4字节指向的是子类B的虚函数表,当调用父类 A的析构函数时,此时对象首地址4字节指向的是类A的虚函数表。因此析构过程大概如下

  1. 先根据子类B的虚函数表(对象首4字节)找到子类B的析构函数,然后执行
  2. 将父类虚函数表的地址覆盖对象首4字节,然后找到父类A的析构函数,并执行

虚函数的模拟实现

下面来模拟c++虚函数的实现,定义一个父类A,和子类 B.C,子类均继承类A。并定义三个全局数组 vtableA,vtableB,vtableC(模拟编译器创建的虚函数表),存放在全局数据,且每个类均有一个4字节的成员变量指针vptr_,在对象初始化的时候,该vptr_指向自己的虚函数表

#include <iostream>

using std::cout;
using std::endl;

typedef void(*fn)(void);

fn vtableA[1];
fn vtableB[1];
fn vtableC[1];

class A {
    public :
        A() {
            vptr_ = (int*)vtableA;
        }

        void WapperFoo() {
            ((fn*)(this->vptr_))[0]();
        }

        static void foo() {
            
          //  cout << "A::foo() Virtual Table address : "
           //      <<(void*)(*reinterpret_cast<int*>(this)) << endl;
           cout << "A::foo()" << endl;
        }

    public :
        int* vptr_;
};

class B : public A {
    public :
        B() {
             vptr_ = (int*)vtableB;
        }

        static void foo() {
             cout << "B::foo()" << endl;
        }
};

class C : public A {
    public :
        C() {
             vptr_ = (int*)vtableC;
        }

        static void foo() {
            cout << "C::foo()" << endl;
        }
};

int main() {
    vtableA[0] = &A::foo;
    vtableB[0] = &B::foo;
    vtableC[0] = &C::foo;

    A* p = new B();
    p->WapperFoo(); // B::foo()
    delete p;

    p = new C();
    p->WapperFoo();  // C::foo()
    delete p;

    return 0;
}
====================
output
B::foo()
C::foo()

程序初始化时,分别填写各个类的虚函数表条目,vtableA的第一个条目为A类的foo()成员函数,vtableB的第一个条目为类B的foo()成员函数,vtableC的第一个条目为类C的foo()成员函数。因此当基类的指针p 指向类B的对象时,调用WapperFoo()(可以看成是编译器封装的foo()函数)函数将调用类B的foo(),同理当p指向类C的对象时,调用WapperFoo()函数将调用类C的foo(),从而实现了运行时多态。可以看到和c语言的多态实现是相似的。

因此从汇编代码(机器码)的角度看,实现多态的本质是在调用callback 函数调用时,是直接调用还是间接调用,直接调用下,函数地址在编译期确定。而在间接调用下,函数地址在运行时确定

# call
call 0x000400008 <fn>  //直接调用,调用函数地址在代码中已确定。编译器确定

## call
call *%rax <fn>    //间接调用,调用地址存储在寄存器/其他内存中,运行时确定

虚函数的真正实现

g++版本:Apple clang version 13.0.0 (clang-1300.0.29.30)

下面的代码基于clang-1300.0.29.3 版本的g++编译,通过对含有虚函数的C++ 进行编译,并通过分析其生成的汇编代码来查看C++的虚函数和多态是如何实现的

C++源程序代码为

#include <iostream>

using namespace std;

class A {
    public :
     virtual void foo() {

     }
     virtual void bar() {
         
     }
     virtual void car() {

     }
};

class B : public A {
    public :
    void foo() {

    }
    void bar() {

    }
    void car() {

    }
};

int foo() {
    static const int a = 999;
    return a;
}

int main() {
    B b;

    A* p = &b;

    p->car();
    return 0;
}

其对应的汇编代码为

.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 11, 0	sdk_version 12, 1
	.globl	__Z3foov                        ## -- Begin function _Z3foov
	.p2align	4, 0x90
__Z3foov:                               ## @_Z3foov
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movl	$999, %eax                      ## imm = 0x3E7
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	_main                           ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$32, %rsp
	movl	$0, -4(%rbp)
	leaq	-16(%rbp), %rdi
	callq	__ZN1BC1Ev
	leaq	-16(%rbp), %rax
	movq	%rax, -24(%rbp)
	movq	-24(%rbp), %rdi
	movq	(%rdi), %rax
	callq	*16(%rax)
	xorl	%eax, %eax
	addq	$32, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1BC1Ev                      ## -- Begin function _ZN1BC1Ev
	.weak_def_can_be_hidden	__ZN1BC1Ev
	.p2align	4, 0x90
__ZN1BC1Ev:                             ## @_ZN1BC1Ev
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rdi
	callq	__ZN1BC2Ev
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1BC2Ev                      ## -- Begin function _ZN1BC2Ev
	.weak_def_can_be_hidden	__ZN1BC2Ev
	.p2align	4, 0x90
__ZN1BC2Ev:                             ## @_ZN1BC2Ev
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rdi
	movq	%rdi, -16(%rbp)                 ## 8-byte Spill
	callq	__ZN1AC2Ev
	movq	-16(%rbp), %rax                 ## 8-byte Reload
	movq	__ZTV1B@GOTPCREL(%rip), %rcx
	addq	$16, %rcx
	movq	%rcx, (%rax)
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1AC2Ev                      ## -- Begin function _ZN1AC2Ev
	.weak_def_can_be_hidden	__ZN1AC2Ev
	.p2align	4, 0x90
__ZN1AC2Ev:                             ## @_ZN1AC2Ev
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	__ZTV1A@GOTPCREL(%rip), %rcx
	addq	$16, %rcx
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rax
	movq	%rcx, (%rax)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1B3fooEv                    ## -- Begin function _ZN1B3fooEv
	.weak_def_can_be_hidden	__ZN1B3fooEv
	.p2align	4, 0x90
__ZN1B3fooEv:                           ## @_ZN1B3fooEv
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	%rdi, -8(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1B3barEv                    ## -- Begin function _ZN1B3barEv
	.weak_def_can_be_hidden	__ZN1B3barEv
	.p2align	4, 0x90
__ZN1B3barEv:                           ## @_ZN1B3barEv
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	%rdi, -8(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1B3carEv                    ## -- Begin function _ZN1B3carEv
	.weak_def_can_be_hidden	__ZN1B3carEv
	.p2align	4, 0x90
__ZN1B3carEv:                           ## @_ZN1B3carEv
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	%rdi, -8(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1A3fooEv                    ## -- Begin function _ZN1A3fooEv
	.weak_def_can_be_hidden	__ZN1A3fooEv
	.p2align	4, 0x90
__ZN1A3fooEv:                           ## @_ZN1A3fooEv
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	%rdi, -8(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1A3barEv                    ## -- Begin function _ZN1A3barEv
	.weak_def_can_be_hidden	__ZN1A3barEv
	.p2align	4, 0x90
__ZN1A3barEv:                           ## @_ZN1A3barEv
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	%rdi, -8(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.globl	__ZN1A3carEv                    ## -- Begin function _ZN1A3carEv
	.weak_def_can_be_hidden	__ZN1A3carEv
	.p2align	4, 0x90
__ZN1A3carEv:                           ## @_ZN1A3carEv
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	movq	%rdi, -8(%rbp)
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.section	__TEXT,__const
	.p2align	2                               ## @_ZZ3foovE1a
__ZZ3foovE1a:
	.long	999                             ## 0x3e7

	.section	__DATA,__const
	.globl	__ZTV1B                         ## @_ZTV1B
	.weak_def_can_be_hidden	__ZTV1B
	.p2align	3
__ZTV1B:
	.quad	0
	.quad	__ZTI1B
	.quad	__ZN1B3fooEv
	.quad	__ZN1B3barEv
	.quad	__ZN1B3carEv

	.section	__TEXT,__const
	.globl	__ZTS1B                         ## @_ZTS1B
	.weak_definition	__ZTS1B
__ZTS1B:
	.asciz	"1B"

	.globl	__ZTS1A                         ## @_ZTS1A
	.weak_definition	__ZTS1A
__ZTS1A:
	.asciz	"1A"

	.section	__DATA,__const
	.globl	__ZTI1A                         ## @_ZTI1A
	.weak_definition	__ZTI1A
	.p2align	3
__ZTI1A:
	.quad	__ZTVN10__cxxabiv117__class_type_infoE+16
	.quad	__ZTS1A

	.globl	__ZTI1B                         ## @_ZTI1B
	.weak_definition	__ZTI1B
	.p2align	3
__ZTI1B:
	.quad	__ZTVN10__cxxabiv120__si_class_type_infoE+16
	.quad	__ZTS1B
	.quad	__ZTI1A

	.globl	__ZTV1A                         ## @_ZTV1A
	.weak_def_can_be_hidden	__ZTV1A
	.p2align	3
__ZTV1A:
	.quad	0
	.quad	__ZTI1A
	.quad	__ZN1A3fooEv
	.quad	__ZN1A3barEv
	.quad	__ZN1A3carEv

.subsections_via_symbols

我们下面来不断的对比汇编代码和C++代码

首先查看 main函数

//C++
int main() {
    B b;

    A* p = &b;

    p->cat();
    return 0;
}

//Assembly
  .globl	_main                           ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	pushq	%rbp                    //保存上一个栈帧的基地址
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp             // rbp寄存器 重新指向当前栈帧的基地址
	.cfi_def_cfa_register %rbp
	subq	$32, %rsp              // main函数所需要的栈空间为32字节,(16字节对齐)
	movl	$0, -4(%rbp)           // 当前函数栈的首4字节置0
	leaq	-16(%rbp), %rdi        // 把对象b的地址传递给 rdi寄存器。作为B构造函数的参数
	callq	__ZN1BC1Ev             // 调用class B的构造函数
	leaq	-16(%rbp), %rax        // 取对象b的地址,暂存在rax寄存中
	movq	%rax, -24(%rbp)        // 将rax内容拷贝给 指针p所指向的栈空间
	movq	-24(%rbp), %rdi        // 指针p的栈地址传递给 rdi寄存器
	movq	(%rdi), %rax// 将指针p指向的内存数据的前8个字节拷贝给rax寄存器,其恰好为B的虚函数表地址
	callq	*16(%rax)  // 由于car虚函数位于B的虚函数表的第三个条目,所以需要+16作为偏移,并调用
	xorl	%eax, %eax
	addq	$32, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

上述main函数中,我们在栈中使用sizeof(B)的大小来分配对象b的内存,其中B本身虽然没有(显示的)成员变量,但是由于虚函数的存在,其对象前8个字节为指向虚函数表的地址,所以sizeof(B)=8(64-bit机器上)。同时定义一个栈变量A* p 来保存对象 b的地址,p为地址类型,64-bit机器上地址占用空间大小8 bytes ,同时为了内存对齐,所以一共需要32字节栈空间大小。

leaq	-16(%rbp), %rdi

即把分配给对象b的栈空间首地址传递给 rdi寄存器,作为下个函数的第一个参数

callq	__ZN1BC1Ev

即为调用 __ZN1BC1Ev函数,通过命令c++filt 可知其为B的构造函数B()

c++filt __ZN1BC1Ev
B::B()

接下来我们看一下函数__ZN1BC1Ev 的汇编实现

  .globl	__ZN1BC1Ev                      ## -- Begin function _ZN1BC1Ev
	.weak_def_can_be_hidden	__ZN1BC1Ev
	.p2align	4, 0x90
__ZN1BC1Ev:                             ## @_ZN1BC1Ev
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rdi
	callq	__ZN1BC2Ev
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

可以看到其最终调用 __ZN1BC2Ev 函数,通过c++filt 发现其亦为B的构造函数B(),进一步查看其汇编函数体

.globl	__ZN1BC2Ev                      ## -- Begin function _ZN1BC2Ev
	.weak_def_can_be_hidden	__ZN1BC2Ev
	.p2align	4, 0x90
__ZN1BC2Ev:                             ## @_ZN1BC2Ev
	.cfi_startproc
## %bb.0:
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rdi
	movq	%rdi, -16(%rbp)                 ## 8-byte Spill
	callq	__ZN1AC2Ev
	movq	-16(%rbp), %rax                 ## 8-byte Reload
	movq	__ZTV1B@GOTPCREL(%rip), %rcx
	addq	$16, %rcx
	movq	%rcx, (%rax)
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function

其主要过程为

  1. 传递对象b的栈空间地址。注。这里在连续的两个8字节栈内存 拷贝了两次 对象b的栈空间地址,对应的汇编代码为 movq %rdi, -8(%rbp)movq %rdi, -16(%rbp)
  2. 调用父类A的构造函数 callq __ZN1AC2Ev
  3. 因为该函数的调用者为B(),所以把class B的虚函数表__ZTV1B的地址取出来,并赋值 rcx寄存器 movq __ZTV1B@GOTPCREL(%rip), %rcx
  4. 然后找到__ZTV1B 虚函数地址条目的开始处 ,并赋值给 对象b的栈空间的首8个字节数据内容

addq $16, %rcxmovq %rcx, (%rax)

我们可以看到 class B的虚函数表__ZTV1B内容为

  .globl	__ZTV1B                         ## @_ZTV1B
	.weak_def_can_be_hidden	__ZTV1B
	.p2align	3
__ZTV1B:
	.quad	0
	.quad	__ZTI1B
	.quad	__ZN1B3fooEv
	.quad	__ZN1B3barEv
	.quad	__ZN1B3carEv

	.section	__TEXT,__const

  .globl	__ZTI1B                         ## @_ZTI1B
	.weak_definition	__ZTI1B
	.p2align	3
__ZTI1B:
	.quad	__ZTVN10__cxxabiv120__si_class_type_infoE+16
	.quad	__ZTS1B
	.quad	__ZTI1A

可以看到其第一个条目为0,第二个条目为 __ZTI1B,其主要保存了类Btypeinfo,typeinfo name等信息,第三个条目开始才是虚函数条目(这里条目存的是地址),这也是为什么要+16的原因(为了跳过前两个条目)。

class B的构造函数B()执行完成后(指令 callq __ZN1BC1Ev 执行后),我们再次回到主函数。

	callq	__ZN1BC1Ev
  leaq	-16(%rbp), %rax        // 取对象b的地址,暂存在rax寄存中
	movq	%rax, -24(%rbp)        // 将rax内容拷贝给 指针p所指向的栈空间
	movq	-24(%rbp), %rdi        // 指针p的栈地址传递给 rdi寄存器
	movq	(%rdi), %rax// 将指针p指向的内存数据的前8个字节拷贝给rax寄存器,其恰好为B的虚函数表地址
	callq	*16(%rax)  // 由于car虚函数位于B的虚函数表的第三个条目,所以需要+16作为偏移,并调用

leaq -16(%rbp), %rax 表示取出 rbp 基地址寄存器向栈顶方向偏移16bytes的地址,这个地址是对象b的栈空间起始地址,将其缓存在 rax寄存器中,并将其值拷贝给 rbp 基地址寄存器向栈顶方向偏移24bytes的栈内存空间,其对应栈变量 A* p 的值,因此对应c++代码

A* p = &b;

接下来 调用指令movq -24(%rbp), %rdi 将指针p的栈空间内容暂存在 rdi寄存器中,并调用 指令movq (%rdi), %rax 将p解引用,取出指针p所指向的数据内容,这其实是class B的虚函数表的地址,即__ZTV1B 中的第三个条目的地址。接着调用指令 callq *16(%rax) 调用最终的虚函数,此虚函数的参数为指针p,16(%rax) 的含义为rax 寄存器指向的地址偏移16个字节(偏移两个条目,car()函数是第三个虚函数) *16(%rax) 表示本次callq 为间接调用方式(无法在编译期确定函数调用地址),这恰好印证了上一节虚函数的模拟实现的推论是正确的。

参考链接

  1. https://stackoverflow.com/questions/66713460/what-does-vargotpcrelrip-mean
  2. http://t.zoukankan.com/tgycoder-p-5238954.html
  3. https://stackoverflow.com/questions/43005411/how-does-the-quad-directive-work-in-assembly
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-05 10:59:30  更:2022-05-05 11:02:37 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 2:10:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码