c++笔记02---内联 inline,动态内存 new/delete,引用,对象,类

本文详细介绍了C++中的内联技术,指出内联可以提升效率但编译器可自行决定是否使用。接着讲解了动态内存分配,对比了new与malloc的区别,并强调了内存释放后指针应置空。此外,还阐述了引用的概念,它是变量的别名,必须初始化且不可改变引用目标。最后,提到了对象和类在面向对象编程中的重要性,以及类的访问控制属性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.    内联 inline
    c 里面的宏嵌入是源码嵌入,内联是二进制嵌入,二进制代替多次调用;
    内联一般用于多次调用的小函数,用空间换时间;
    注意:递归函数无法作内联,递归是自己调自己,所以无法自己嵌自己;
    通过 inline 关键字,可以建议编译器对函数进行内联,但是仅仅是建议,编译器可以不执行;
        inline void foo(int x) {...}
    现在的编译器会自己判断是否需要内联;

2.    动态内存分配 new/delete(操作符)
    c 里面用下面几个函数动态分配内存,位于堆里面,c++ 同样可以使用,需要包含头文件 #include <cstdlib>
    malloc(不清零)/calloc(清零)/realloc/free
        int *p = (int *)malloc(1 * sizeof(int));        // c++里不推荐使用
    
    new 和 malloc 区别:
    1)new 是运算符,malloc 是函数;
    2)new 最终还是调用 malloc 分配内存;
    3)malloc 仅仅是分配内存,new 还调用构造函数;

    c++里主要用下面函数进行内存分配和释放:(不用包含任何头文件)
    new/delete :对单个变量进行内存分配/释放,上面那段 malloc 代码可以写为:
        int *p = new int;
        delete p;
        p = NULL;
    也可以如下初始化:(有待验证)
        int *p = new int (56);        // 对p进行赋初值

    下面两种变量初始化方式都正确:
        int i = 10;
        int i(10);

    一般情况下,我们 delete 完了,就要把指针清零置空;
        int *p = new int;
        delete p;
        *p = 2000;            // 这种情况下,p属于野指针,无法确定结果,属于未定义类型;

        int *p = new int;
        delete p;
        p = NULL;
        *p = 2000;            // 这种情况下,p为零,结果确定,返回段错误;    

    对数组进行内存分配/释放:new[]/delete[]:
        p = new int [10];
        delete[] p;            // 如果此处不带[ ],那么结果不确定,属于未定义;多维数组也用这条语句释放,不用多加[ ]
        p = NULL;
        
    注意:
        delete[] p;    相当于:    delete (p-4);
    记着,这里 p 前面有 4 个字节用于存放数组大小;
    
3.    定位分配:在指定位置分配内存空间;(了解)
    并不是所有 new 分配出来的空间都在堆里面;
    char buf[4] = {0x12, 0x34, 0x99, 0x23};     // buf里面放了四个数,buf 位于栈里面
    int *p = new (buf) int;                        // 在 buf 里面分配一个 int 空间给 p,所以这个空间在栈里面
    delete p;                                    // error,段错误,栈不需要释放
    
4.    引用即别名;
        int a = 20;
        int &b = a;            // 引用,b就是a的别名
        b = 10;                // 此时 a 值为10
        
    引用必须初始化,且非常引用必须用变量初始化;
    只有常引用可以用常量初始化;
    
    一旦初始化,就不能再引用其他变量;
        int &b;                // error
        int &b = 1000;        // error
        const int &b = 100;    // ok
        
    引用是在底层其实就是用指针来实现的。

    c++ 和 c 相比,就多加了两个变量,一个是布尔,另一个是引用;

5.    引用应用场景:
    1)引用型参数:
        a.修改实参
        b.避免拷贝
            通过 const 可以防止在函数中意外地修改实参的值;
            同时还可以接收拥有常属性的实参;如下:
                void foo(const int &a) {...}    // 增加 const,foo 的实参范围变大了,既可以传变量,也可以传常量
                // 如果 foo 函数体内需要改变 a 的值,我们则不能加 const
                int main() { foo(100); }
    2)引用型返回值:
        int data = 100;                            // data必须是全局变量(原因见下面)
        int &foo() { return data; }                // 返回引用,相当于返回 data 本身;
        int main() { foo() = 200; }                // 如果foo不是引用,则无法把 200 返回给data,同时data要是全局变量才行

        从一个函数中返回引用,往往是为了将该函数的返回值作为左值使用;
        但是,一定要保证函数所返回的引用的目标在该函数返回以后依然有定义,否则将导致不确定后果;
        也就是说,不要返回局部变量;
        除非局部变量是静态的,或是在动态内存中分配的;
        可以返回全局、静态、成员变量的引用,也可以返回引用型行参;
        int &foo() { int a;  return a; }    // 无效,a是局部变量
        int &foo(int a) { return a; }        // 无效,a是执行型行参
        int &foo(int &a) { return a; }        // 有效,a是引用型行参

        函数的行参可以是引用,而通过引用传递参数,称之为引用传递;
            int &i = foo();                        // ok
            int j = foo();                        // ok
        在引用传递时,经常使用 const 来保护引用参数的传递;
        在C++中,尽量使用引用传递参数,尽量使用引用代替指针;
        
        example:
        int& foo (const int& a) { return a; }
        int main () { foo (100); }
        编译提示错误:传进去的是 const a,那么返回的应该也是 const 型;
        在函数前面加上 const 就可以了:
            const int& foo (const int& a) { return a; }
        tips:这个例题实际意义不大,因为函数返回类型为引用,目的是为了做左值(可以修改);
        但这里又加了 const 属性,让其不能做左值,矛盾;
        所以这个例题只是说明,如果 return const 类型,那么函数返回值也必须是 const;

6.    引用的本质就是指针,很多场合下是可以互换的;
    在高级语言 c++ 层面上,他们还是有区别的,如下:
    1)指针是实体变量,但是引用不是实体变量,只是一个别名;
        int &a = i;     sizeof(a);                      // a 即(int)i 的大小 4
        double &b = f;     sizeof(b);                        // b 即(double)f 的大小 8
    2)指针是实体,可以不初始化(但最好初始化);引用必须初始化;
    3)指针的目标可以修改,但是引用的目标不能修改;
        int&a = i;    ==>    int* const a = &i;                // a 不能再指向别的变量
    4)可以定义指针的指针,也就是二级指针;但是不可以定义引用的指针;
        int &r = a;    int&* p = &r;                        // 不对,r不是实体,没有地址,也就没有指针
    5)可以定义指针的引用,但是不可以定义引用的引用;
        int a;     int *p = &a;     int*& q = p;            // OK
        int a;     int &r = a;     int&& s = r;            // ERROR,但是 int &s = r; 是正确的;
    6)可以定义指针的数组,但是不能定义引用的数组;
        int a, b, c;     int *parr[] = {&a, &b, &c};        // OK
        int a, b, c;     int &rarr[] = {a, b, c};        // ERROR
        可以定义数组的引用:
        int arr[] = {1, 3, 5};     int (&arr1)[3] = arr;    // OK
        
    举例 1
        int a = 100, b = 200;
        int *p1 = &a, *p2 = &b;
        p1 = p2;                        // p1 指向 b 地址,但是 a 的值不变
        int &r1 = a, &r2 = b;
        r1 = r2;                        // 引用改变原对象值,此时 a = 200;

7.    显示类型转换运算符
    c里面叫做强制类型转换;
    c++里面分为以下几种类型转换:

    1)静态类型转换
        static_cast < 目标类型 >( 源类型变量 )
        转换时作静态检查,即在编译时进行;
        void * 到其他指针的转换;
        如果在目标类型和源类型之间,某一个方向上可以作隐式类型转换,那么两个方向上都可以作静态类型转换;
        如果在两个方向上都不能作隐式类型转换,那么在任意一个方向上也不能作静态类型转换;
        int *p1 = ...;    void *p2 = p1;
        p1 = static_cast<int *>(p2);
        如果存在从源类型到目标类型的自定义转换规则,那么也可以使用静态类型转换;(了解,后面再讲)

    2)动态类型转换(后期多态部分讲)
        dynamic_cast < 目标类型 >( 源类型变量 )
        主要用于具有多态性的父子类指针或引用之间;

    3)常类型转换
        const_cast < 目标类型 >( 源类型变量 )
        给一个拥有const属性的指针或引用去掉常属性;

        int a = 100;
        const int *p1 = &a;                    // ok,常指针可以指非常;反过来不行,看下面例子
        *p1 = 200;                            // error, p1不能改
        int *p2 = p1;                            // error,p1是常属性,p2不是,两者属性不匹配
        int *p2 = const_cast<int *>(p1);
        *p2 = 200;                            // ok,p2被去常了

        const int b = 100;
        int *p3 = &b;                            // error,b是const型,不能赋值给非常p3

        常量和拥有常属性的变量是不一样的;后者在特定情况下可以改变,而前者无法改变;
        c++ 编译器特有的常量优化:
        const int a = 100;
        int *p1 = const_cast<int *>(&a);
        *p1 = 200;
        cout << a;                            // 100, 编译器默认为凡是有a的地方,都提前换为100 :cout << 100;
        cout << *p1;                            // 200
        
        上面给 a 加了 const 属性,如果给 a 再加上 volatile 后,const volatile int a = 100;
        编译器就不对 a 进行常量优化,那么最后 a 的值就是 200;(和硬件相关的时候用到)

        注意:const_cast 只是去常,不能进行类型转换,也就是说,不能写为:int *p2 = const_cast<double *>(p1);
        或者如果 p1 为 double 型,也不能写成:int *p2 = const_cast<int *>(p1);
        
        问:关键字 volatile 有什么含意 并给出三个不同的例子 ?
        答:一个定义为 volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
        精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
        下面是 volatile 变量的几个例子:
            1) 并行设备的硬件寄存器(如:状态寄存器)
            2) 一个中断服务子程序中会访问到的非自动变量 (Non-automatic variables)
            3) 多线程应用中被几个任务共享的变量
        回答不出这个问题的人是不会被雇佣的。我认为这是区分 C 程序员和嵌入式系统程序员的最基本的问题。
        嵌入式系统程序员经常同硬件、中断、RTOS 等等打交道,所用这些都要求 volatile 变量。
        不懂得 volatile 内容将会带来灾难。
        假设被面试者正确地回答了这个问题(嗯,怀疑这否会是这样),我将稍微深究一下,
        看一下这家伙是不是真正懂得 volatile 完全的重要性。
            1) 一个参数既可以是 const 还可以是 volatile 吗?解释为什么。
            2) 一个指针可以是 volatile 吗?解释为什么。
            3) 下面的函数有什么错误:
            int square (volatile int *ptr) {
                return *ptr * *ptr;
            }
            下面是答案:
            1) 是的。一个例子是只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。
            它是 const 因为程序不应该试图去修改它。
            2) 是的。尽管这并不很常见。一个例子是当一个中服务子程序修改一个指向一个 buffer 的指针时。
            3) 这段代码有个恶作剧。这段代码的目的是用来返回指针 *ptr 指向值的平方,
            但是,由于 *ptr 指向一个 volatile 型参数,编译器将产生类似下面的代码:
            int square (volatile int *ptr) {
                int a, b;
                a = *ptr;
                b = *ptr;
                return a * b;
            }
            由于 *ptr 的值可能被意想不到地该变,因此 a 和 b 可能是不同的。
            结果,这段代码可能返回不是你所期望的平方值!正确的代码如下:
            long square (volatile int *ptr) {
                int a;
                a = *ptr;
                return a * a;
            }

    4)重解释类型转换
        reinterpret_cast < 目标类型 >( 源类型变量 )
        在不同类型的指针或引用之间作类型转换,以及在指针和整型之间作类型转换;
            可以强转任何类型的指针;
            把整数强转成指针;或者把指针强转成整数;
        int i = 0x12345678;
        char *p = reinterpret_cast<char *>(&i);    // 把int看作4个char
        for (size_t i = 0; i < 4; ++i)
            cout << hex << (int)p[i] << ' ';        // 输出:78 56 34 12(小端)
        float *p = reinterpret_cast<float *>(&i);
            cout << *p << endl;
        void *v = reinterpret_cast<void *>(i);
            cout << v << endl;

    5)目标类型变量 = 目标类型(源类型变量);    // c里面强制类型转换是:目标类型变量 = (目标类型)源类型变量;
        int a = int (3.14);                    // a = 3;

        char c = 'a';
        int n = (int)c;    // c还是char型,临时生成一个临时变量/匿名变量,强制为int型,值和char c是一样的
        (int)c = 100;        // erro,匿名变量都是右值,不能被赋值;

        所有的匿名变量都是右值,如果要引用它,需要带有 const
        void foo(int &a){...}                // void foo(const int &a){...}
        char c = 'a';
        foo(c);                                // error,类型不一致
        foo((int)c);                            // 实参是int型临时变量,是右值,行参是左值引用,如果在行参里面加const就对了

8.    c++ 之父的建议:
    1)少用宏,多用 const、enum、inline;
        #define PI     3.14
        const double PI = 3.14;
        #define ERROR     -1
        enum{ ERROR = -1 };
        #define max(a, b)        ((a) > (b) ? (a) : (b))
        inline int double max(double a, double b){ return a > b ? a : b; }
        
        对于单纯常量,最好以 const 或 enum 替换;
        对于形似函数的宏,最好用 inline 函数替换;
        
    2)变量随用随声明,同时初始化;
    3)少用 malloc/free,多用 new/delete
    4)少用c风格的强制类型转换,多用类型转换运算符;
    5)少用c风格的字符串,"\n,\0",多用string;
    6)少用数组;
    7)树立面向对象的编程思想;
    
    问:请说出 const 与 #define 相比,有何优点 ?
    答:const 作用:定义常量、修饰函数参数、修饰函数返回值三个作用。
    被 const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
    1)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。
    而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
    2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

9.    对象---面向对象程序设计(OOP)
    1)程序就是一组对象,对象之间通过消息交换信息;
    2)类就是对对象的描述和抽象,对象就是对类的具体化和实例化;
    3)c++中对象其实就是一个变量,在计算机中是一片内存区域;
    4)类通过属性和行为两方面对对象进行抽象;
        c++中,对象属性就是成员变量;对象行为就是成员函数;
    判断一个程序是否是面向对象,判断依据看其是否具有:封装,继承,多态;

10.    面向对象程序设计(OOP)学习过程:
    1)至少掌握一种OOP变成语言;
    2)精通一种面向对象的元语言,如UML;
    3)研究设计模式,如GOF;

11.    类的定义:
    class 类名 {
        类型 成员变量名;                            // 不能在此初始化
        返回类型 成员函数名(行参表){函数体};        // 通过函数初始化成员变量,一般是构造函数
    };                                                // 末尾的分号别忘记
    类是创建对象的模型;
    c++中类就是用户自定义的数据类型;

    成员变量如果不初始化,结果不确定;
        int --- 随机数
        char --- \0 或者 NULL

    c++里,变量有成员、局部和全局之分;
    函数有成员函数和全局函数之分;没有局部函数;

12.    访问控制属性:
    public: 公有成员,大家都可以访问;
    private: 私有成员,只有自己可以访问;(缺省属性)
    protected: 保护成员,自己和自己的子类可以访问;

    class 类的缺省访控属性是 private,struct 结构的缺省访控属性是 public,用于和c兼容;

    class Student{
    public:
        string m_name;
        int m_age;
        void eat(const string &food)
        { cout << food << endl; }
    };
    int main()
    {
        Student student = {"zhangfei", 23};
        Student student;                                // student就是实例化对象
        student.m_name = "zhangfei";
        student.m_age = 23;
        student.eat("beer");
        return 0;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值