C++面试——C/C++ 基础之一

关于C/C++ 基础的基本资料 之一

1.谈谈对C++内存分配的理解



   1.1 还是的先看看C++对内存分为哪几个区?

    真正合理的C++的内存划分为栈区、堆区、全局区/静态区、字符串常量和代码区。

    1、栈区(stack)——— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。想知道为什么效率高吗?因为关于栈的操作如push集成在处理器的指令集中,效率很高,但是分配的内存容量有限。
                栈区:由系统进行内存的管理。
                说明:主要存放函数的参数以及局部变量。栈区由系统进行内存管理,在函数完成执行,系统自行释放栈区内存,不需要用户管理。整个程序的栈区的大小可以在编译器中由用户自行设定,默认的栈区大小为3M。

    2、堆区(heap) ——— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
                堆区:由用户手动申请,手动释放。在C中使用malloc,在C++中使用new(当然C++中也可以使用malloc)。
                说明:new操作符本质上还是使用了malloc进行内存的申请,因此我将自由存储区和堆区都说成堆区,不过两者还是有很大的差别。
                1)malloc是C语言中的函数,而new是C++中的操作符。
                2)malloc申请之后返回的类型是VOID*,而new返回的指针带有类型。
                3)malloc只负责内存的分配而不会调用类的构造函数,而new不仅会分配内存,而且会自动调用类的构造函数。

    3、全局区(静态区)(static)———,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 
                全局/静态区:全局、静态数据存放在一起的,初始化的全局变量和静态变量是在一起的。未初始化的全局变量和静态变量是在相邻的空间中。
                说明:全局变量和静态全局变量的存储方式是一致的,但是其区别在于,全局变量在整个源代码中都可以使用,而静态全局变量只能在当前文件中有效。比如我们的一个程序有5个文件,那么某个文件中申请了静态全局变量,这个静态全局变量只能在当前文件中使用,其他四个文件均不可以使用。而某个文件中申请了全局变量,那么其他四个文件中都可以使用该全局变量(只需要通过关键字extern申明一下就可以使用了)。事实上static改变了变量的作用范围。

    4、文字常量区———常量字符串就是放在这里的。
                字符串常量区:存放字符串常量,程序结束后,由系统进行释放。
                说明:比如我们定义char * p = “Hello World”; 这里的“Hello World”就是在字符串常量中,最终系统会自动释放。

    5、程序代码区———存放函数体的二进制代码。
                代码区:存放程序体的二进制代码。
                说明:比如我们写的函数,都是在代码区的。
               

《Unix环境高级编程》(著名的APUE)中提出:
        C程序的内存布局:Text,Data,BSS,Stack, Heap.
        Text是程序的代码段。
        Data是程序中初始化了的全局、静态数据变量。
        BSS是程序中未初始化的全局、静态数据变量。即使全局、静态数据变量初始化为0仍然是属于BSS段。(未初始化的数据段)
        Stack是程序中的局部变量,由高地址到低地址向下增长。
        Heap是malloc调用动态分配的内存,由低地址到高地址向上增长。


    根据网上别人的观点结合自己的测试,得出的结论:
        1、 经过初始化的全局变量和静态变量保存在数据段中。
        2、 未经初始化的全局变量和静态变量保存在BSS段。
        3、 函数内部声明的局部变量保存在堆栈段(栈)中,函数调用相关信息也在栈中。
        4、 const修饰的全局变量保存在文本段中,const修饰的局部变量保存在堆栈段中。
        5、 字符串常量保存在文本段中。 


    查看各段的大小用size命令:size test.out  (test.out为编译连接后的可执行文件),可以查看到text、data、bss段的大小。

     
    1.2 再谈谈new/delete和malloc/free的区别?

        1、像我们的new/delete和malloc/free就是在上面所说的堆区上操作。程序员申请了内存,用完要记得释放,否则就内存泄露了。而且多次申请释放,会造成堆区碎片,这个需要注意下。
        2、new/delete是操作符,而malloc/free是函数。前者可以被重载。前者可以有构造函数和析构函数。前者返回的某种类型对象的指针,后者返回VOID指针。


2.基于问题1,深入谈谈堆和栈

1、申请方式:
    栈:函数内的局部变量:int a = 10;
    堆:new / malloc

2、申请后的系统响应:
    栈:只要申请的空间大小<栈的剩余空间,栈就分配。
    堆:因为new,malloc都是C++封装的,里面做了这样的事:首先明白windows有这么一个记录空闲空间地址的链表,C++遍历该链表,先找到第一个空闲空间大小大于程序员申请的空间大小的地址(堆节点),将该堆节点从链表中删除,把该节点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

3、申请大小:
    栈:默认是1M?还是2M?
    堆:看系统的虚拟内存有多大了。(请记住堆是一个节点为空闲内存空间的链表。。)堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。


4、申请效率:
    栈:速度快,push,pop等指令都内置在处理器了,能不快吗?
    堆:相对慢(为什么慢,可以看2,就知道它在申请的时候干了多少事),还容易产生内存碎片。不过容量大,操作方便。

5、存储内容:
    栈(依次是):
        主函数中后的下一条指令地址
        然后是函数的各个参数(在大多数的C编译器中,参数是由右往左入栈的)
        函数中的局部变量
    堆:程序员随意安排。

6、存取效率:
    先看看这段代码:

        int main(){
         char a = 1;
         char c[] = "1234567890";
         char *p ="1234567890";
         a = c[1];
         a = p[1];
         return 0;
        }

    这是在函数内,char数组c,是在栈上。

    char *p = "1234567890" ,其中虽然*p也是在栈上,但是"1234567890"在常量区。
     
    再看看它们对应的汇编代码:

        10: a = c[1];
        00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
        0040106A 88 4D FC mov byte ptr [ebp-4],cl
        11: a = p[1];
        0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
        00401070 8A 42 01 mov al,byte ptr [edx+1]
        00401073 88 45 FC mov byte ptr [ebp-4],al

    可以看到在栈上的c[]只要两行就能取到内容,而p要先去把常量区的内容放到edx中,再存取。
    说明纯栈操作,效率是要高一些的。
 
7、总结
 
    看看一个经典代码就什么都懂了:

        //main.cpp 
        int a = 0; //全局初始化区 
        char *p1; //全局未初始化区 
        main() 
        { 
         int b; //栈 
         char s[] = "abc"; //栈 
         char *p2; //栈 
         char *p3 = "123456"; //123456\0在常量区,p3在栈上。 
         static int c =0; //全局(静态)初始化区 
         p1 = (char *)malloc(10); 
         p2 = (char *)malloc(20); 
         //分配得来得10和20字节的区域就在堆区。 
         strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
        }


3.谈谈这strcpy,memcpy和sprintf三者的区别?

    共同点:都是拷贝东西。
    效率排行: memcpy > strcpy > sprintf
    操作对象:memcpy是两块内存之间的操作,strcpy 是两个字符串对象之间的操作,sprintf是任意类型转到字符串的操作。


4.C++多态机制
    先上代码:

        #include <iostream.h>
        class animal
        {
        public:
            void sleep()
            {
                cout<<"animal sleep"<<endl;
            }
            void breathe()
            {
                cout<<"animal breathe"<<endl;
            }
        };

        class fish:public animal
        {
        public:
            void breathe()
            {
                cout<<"fish bubble"<<endl;
            }
        };

        int main()
        {
            fish fh;
            animal *pAn=&fh;
            pAn->breathe();
            return 0;
        }

    运行结果是:


内存模式:

    子类fish的内存模型是,先animal,然后再自己增加的部分。因为:
        animal *pAn=&fh;
    fish对象被强制更改成父类animal对象,指针指向fish的上半部分(那不就是一个animal对象了吗?),所以pAn->breathe()就是父类的breathe()。


    更深入的了解机制(早绑定和晚绑定):
     
    早绑定:C++在编译的时候,要确定每个对象调用的函数的地址,这就是早绑定。
    晚绑定:编译的时候先不决定,等到运行的时候,再根据情况确定要调用的函数地址,这就是晚绑定。
     
    什么东西支持晚绑定这个机制呢?虚函数!
    虚函数机制,每个类对象有个虚表指针:vptr,这个指针指向类所属的虚表:vtable。
     
    当我们把父类animal的breathe()设置为虚函数,子类fish即使被强制转换成animal对象,也是没关系的。
    因为pAn实际指向的就是fish对象!
    因此vptr指向的是fish的vtable!
    所以vptr指向的vtable中的breathe()函数就是fish对象的breathe()函数。
     
    正确的多态代码:
     
        #include <iostream.h>
        class animal
        {
        public:
            void sleep()
            {
                cout<<"animal sleep"<<endl;
            }
            virtual void breathe()
            {
                cout<<"animal breathe"<<endl;
            }
        };

        class fish:public animal
        {
        public:
            void breathe()
            {
                cout<<"fish bubble"<<endl;
            }
        };

        int main()
        {
            fish fh;
            animal *pAn=&fh;
            pAn->breathe();
            return 0;
        }

    运行结果:
    
    
    
    
    再一次深入了解vptr和vtable:

    每个含有virtual function的类中都隐式包含着:
    一个静态虚指针vfptr指向该类的静态虚表vtable
     vtable中的表项指向类中的每个virtual function的入口地址
     
    每个类内部都有一个虚表,无论类型怎么被转换,虚表指针vptr都是固定的。(fish被转换成animal也无所谓,vptr指向的永远是fish的vtable)

    关于虚表:
        1、每一个类都有虚表。
        2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
        3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
    
    
5.C++的虚函数和纯虚函数


    1、后者不可实例化:虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类(抽象类不可实例化)。

    2、后者不可被直接使用:虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。

    3、两者都是可以被重载,体现多态:虚函数和纯虚函数都可以在子类中被重载(虚函数可以不被重载,纯虚函数必须在子类实现),以多态的形式被调用。
     
    4、形式上:virtual{};纯虚函数的定义形式:virtual  { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。(vitrual 和 static 就是死对头)

    5、意义上:定义纯虚函数就是为了让基类不可实例化(没意义),因为实例化这样的抽象数据结构本身并没有意义或者给出实现也没有意义。

    个人感觉,很多时候,基类,它能够实例化对象就是个不合理的存在,比如:动物,你给我实例化一个动物看看?
     
     
6.引用ref


    1、什么是“引用”?声明和使用“引用”要注意哪些问题?
        引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。声明一个引用的时候,切记要对其进行初始化。
        引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。
        声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
     
    2、将“引用”作为函数参数有哪些特点?
        (1)传递引用给函数与传递指针的效果是一样的。

        这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

        (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;(无需副本)
        而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

        (3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。(阅读障碍啊)
     
     
    3、什么时候用“常引用"?

        const 类型标识符 &引用名=目标变量名;
        int a ;
        const int &ra=a;
        ra=1; //错误
        a=1; //正确
     
    既要保证效率,又要保证传递的数据不能被改变。
     
        string foo( );
        void bar(string & s); 
        bar(foo( )); //错误
        bar("hello world");//错误
     
    因为bar中参数是非const类型,而string的临时对象都是const类型,所以把const类型对象赋值给非const类型对象,是编译不过的。
     
     
    4、将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
        (1)不能返回局部变量的引用。

        主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。 
         

        (2)不能返回函数内部new分配的内存的引用。
        虽然不存在局部变量的被动销毁问题,可是有出现了其他问题:
        被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
         
        (3)流操作符重载返回值声明为引用 【必须用引用】
        估计就是C++引入”引用“这个概念主要原因吧。
        流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。
        为什么不是流对象呢?
        每次返回值都是一个流对象的话,都要一次拷贝构造函数,每次的<< 都是不同的流对象,从开销和效率上看,这显然不可取。
        为什么不是流对象指针呢?
        指针能<<连续两次吗?如果针对流对象能的话,要修改整个指针机制,这显然不可取。
        所以流对象引用,才是唯一可取的方法!
         
        (4)赋值操作符 = 返回值也是声明为引用【必须用引用】
        因为它和流操作符一样,都可以连续操作: x = y = 1。用引用,很科学。理由如(3)。
         
        (5)四则运算符 + - * /  的返回值不能使引用【必须不能用引用】
        主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:
        返回一个对象、
        返回一个局部变量的引用、
        返回一个new分配的对象的引用、
        返回一个静态对象引用。
        根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。
        所以可选的只剩下返回一个对象了。
        
    
    5、引用与多态的关系?
        引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。


        Class A; 
        Class B:Class A{...};  
        B b; 
        A& ref = b;


    6、引用与指针的区别?
        指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。
        程序中使用指针,程序的可读性差;
        而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
        此外,就是上面提到的对函数传ref和pointer的区别。 
 


7.实现strcpy
    char *strcpy(char *strDest, const char *strSrc)
    {
     if ( strDest == NULL || strSrc == NULL)
      return NULL ;
     if ( strDest == strSrc)
      return strDest ;
     char *tempptr = strDest ;
     while( (*strDest++ = *strSrC++) != ‘\0’)
     ;
     return tempptr ;
    }


8.实现String类
    一直都有这么一个印象:拷贝构造函数和赋值函数是一对好基友。
    class String
    {
        public:
        String(const char *str = NULL); // 通用构造函数
        String(const String &another); // 拷贝构造函数
        ~ String(); // 析构函数
        String & operater =(const String &rhs); // 赋值函数
        private:
        char *m_data; // 用于保存字符串
    };

    String::String(const char *str)
    {
      if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
        {
          m_data = new char[1] ;
          m_data[0] = '\0' ;
        }
      else
        {
          m_data = new char[strlen(str) + 1];
          strcpy(m_data,str);
        }
    }

    String::String(const String &another)          //拷贝构造函数
    {
        m_data = new char[strlen(another.m_data) + 1];
        strcpy(m_data,other.m_data);
    }

    String& String::operator =(const String &rhs)  //赋值函数  
    {
        if ( this == &rhs)
            return *this ;
        delete []m_data; //删除原来的数据,新开一块内存
        m_data = new char[strlen(rhs.m_data) + 1];
        strcpy(m_data,rhs.m_data);
        return *this ;
    }

    String::~String()
    {
        delete []m_data ;
    }


9.#include<file.h> 与 #include "file.h"的区别?
    答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。
 
 
10.C++用C编译器编译后的函数为什么要加入extend "C"?
    两者编译机制不一样,为了能够和谐工作,extend "c"告诉C++编译器,这里按照C编译器的处理方式编译链接就行了。
    一句话:实现C++与C及其它语言的混合编程。
    (我在JNI的时候用到这玩意~)
     
    为什么?
     
    作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。
    函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为: 
    void foo( int x, int y );  
    该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
     
    现在假设模块A是C语言编译后的,模块B是C++模块。B中调用A的函数。
     
    这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;
    它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
 
 
11.面向对象的三个基本特征,并简单叙述之?
    1. 封装:
    将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)

    2. 继承:
    广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。

    3. 多态:
    是将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
    简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
 
 
12. 重载(overload)和重写(overried)的区别?
    从定义上来说:
    重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
    重写:是指子类重新定义父类虚函数的方法。

    从实现原理上来说:
    重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
    重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。


13. 如何判断一段程序是由C 编译程序还是由C++编译程序编译的?
    #ifdef __cplusplus
    cout<<"C++";
    #else
    cout<<"c";
    #endif
    

《C++ 设计新思维》 下载见 http://www.linuxidc.com/Linux/2014-07/104850.htm
    C++ Primer Plus 第6版 中文版 清晰有书签PDF+源代码 http://www.linuxidc.com/Linux/2014-05/101227.htm
    读C++ Primer 之构造函数陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm
    读C++ Primer 之智能指针 http://www.linuxidc.com/Linux/2011-08/40177.htm
    读C++ Primer 之句柄类 http://www.linuxidc.com/Linux/2011-08/40175.htm

将C语言梳理一下,分布在以下10个章节中:
    Linux-C成长之路(一):Linux下C编程概要 http://www.linuxidc.com/Linux/2014-05/101242.htm
    Linux-C成长之路(二):基本数据类型 http://www.linuxidc.com/Linux/2014-05/101242p2.htm
    Linux-C成长之路(三):基本IO函数操作 http://www.linuxidc.com/Linux/2014-05/101242p3.htm
    Linux-C成长之路(四):运算符 http://www.linuxidc.com/Linux/2014-05/101242p4.htm
    Linux-C成长之路(五):控制流 http://www.linuxidc.com/Linux/2014-05/101242p5.htm
    Linux-C成长之路(六):函数要义 http://www.linuxidc.com/Linux/2014-05/101242p6.htm
    Linux-C成长之路(七):数组与指针 http://www.linuxidc.com/Linux/2014-05/101242p7.htm
    Linux-C成长之路(八):存储类,动态内存 http://www.linuxidc.com/Linux/2014-05/101242p8.htm
    Linux-C成长之路(九):复合数据类型 http://www.linuxidc.com/Linux/2014-05/101242p9.htm

 摘选自 : http://www.linuxidc.com/Linux/2014-11/109161.htm

         http://www.cnblogs.com/madonion/articles/2269195.html




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
面向对象的编程中,C语言并不直接支持类和抽象的概念。引用中提到,final关键字用来修饰方法,表示该方法不能在子类中被覆盖。而abstract关键字用来修饰抽象方法,表示该方法必须在子类中被实现。然而,在C语言中,没有对应的关键字来实现类和抽象的概念。 相反,C语言通过结构体来模拟类的概念。结构体是一种用户自定义的数据类型,可以包含多个不同类型的数据成员。通过结构体,我们可以将相关的数据和功能组合在一起。然而,C语言中的结构体不支持继承和多态等面向对象的特性。 在C语言中,我们可以使用函数指针来模拟抽象类和接口的概念。函数指针可以指向不同的函数,通过使用函数指针,我们可以实现多态性,即在运行时根据函数指针指向的具体函数来执行不同的操作。 综上所述,C语言并不直接支持面向对象中的类和抽象的概念,但可以使用结构体和函数指针来实现类似的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [面向对象——类和对象](https://blog.csdn.net/shouyeren_st/article/details/126210622)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [面向对象编程原则(06)——依赖倒转原则](https://blog.csdn.net/lfdfhl/article/details/126673771)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值