【C++】类和动态内存分配

动态内存和类

  1. 静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本,即类的所有对象共享同一个静态成员。
  2. 不能在类声明中初始化静态成员变量;对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分
    int StringBad::num_strings = 0;//初始化指出类型,并使用作用域运算符,但没有使用关键字static

静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态类成员所属的类。但如果静态成员是const 整数类型或枚举型,则可以在类声明中初始化。

  1. 删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。在构造函数中使用new分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[](包括中括号)来分配内存,则应使用delete[](包括中括号)来释放内存。
  2. 特殊成员函数
    C++自动提供以下函数(在没有定义的情况下):默认构造函数,默认析构函数、复制构造函数、赋值运算符、地址运算符;
    a.默认构造函数:不接受任何参数也不执行任何操作,带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值,但只能有一个默认构造函数。
    b.类的复制构造函数:原型:Class_name(const Class name&);其接受一个指向类对象的常量引用作为参数。按值传递意味着创建原始变量的一个副本,当按值传递和返回对象时,都将调用复制构造函数。由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。深度复制:复制构造函数应当复制字符串并将副本的地址赋给str成员,而不是仅仅复制字符串地址。如:
StringBad::StringBad(const StringBad & st)
{
  num_strings++; //handle static member update
  len=st.len;  //same length
  str=new char [len+1];  //allot space
  std::strcpy(str,st.str); //copy string to new location
  cout<<num_strings<<: \””<<str<<”\”object created\n”; //for your information
}

必须定义复制构造函数的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身。警告:如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。
c.赋值运算符 原型: class_name & class_name::operator=(const class_name &);它接受并返回一个指向类对象的引用。对于由于默认赋值运算符不合适而导致的问题,解决办法时提供赋值运算符(进行深度复制)定义。其实现与复制构造函数类似,但也有一些差别。

由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。
函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
函数返回一个指向调用对象的引用。

改进后的String类

C++11提供了引入关键字nullptr,用于表示空指针。

  1. 使用中括号表示法访问字符
    可以使用方法operator[]()来重载该运算符,通常,二元C++运算符(带两个操作数)位于两个操作数之间,如2+5. 但对于中括号运算符,一个操作数位于第一个中括号的前面,另一个操作数位于两个中括号之间。
  2. 静态类成员函数
    可以将成员函数声明为静态的(函数声明必须包含关键字static,但如果函数定义是独立的则不能包含)。不能通过对象调用静态成员函数;如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。同样,也可以使用静态成员函数设置类级(classwide)标记,以控制某些类接口的行为,例如,类级标记可以控制显示类内容的方法所使用的格式。

在构造函数中使用new时应注意的事项

1.在使用new初始化对象的指针成员时要特别小心!

  • 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
  • new和delete必须相互兼容。new对应deletenew[]对应delete[]
  • 若有多个构造函数,则必须以相同的方式使用 new,要么都带中括号,要么都不带(所有构造函数都必须与一个析构函数兼容)。可在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或nullptr),这是因为delete(无论是否有中括号)都可用于空指针。
  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
  • 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。
    例如:
 String & String::operator=(const String & st)
  {
    if(this == & st) //object assigned to itself
        return *this;
    delete [ ] str; //free old string
    len = st.len;
    str=new char [len + 1]; //get space for new string
    std::strcpy(str, st.str); //copy the string
    return * this; //return reference to invoking object
}

检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
3. 有关返回对象的说明
当成员函数或独立的函数返回对象几种方式:返回指向对象的引用、指向对象的const引用或const对象。

  • 返回指向const对象的引用(提高效率):返回对象将调用复制构造函数,而返回引用不会;引用指向的对象应该在调用函数执行时存在。
  • 返回指向非const对象的引用:两种情形,重载赋值运算符以及重载与cout一起使用的<<运算符。
  • 返回对象:如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它。

总之,如果方法或函数要返回局部变量,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。最后,有些方法和函数(如重载的赋值运算符)可以返回对象,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。

使用指向对象的指针

1.使用对象指针需注意

  • 使用常规表示法来声明指向对象的指针:
    String * glamour;
  • 可以将指针初始化为指向已有的对象:
    String * first = &sayings[0];
  • 可以使用new来初始化指针,这将创建一个新的对象:
    String * favorite = new String (sayings[choice]);
  • 对类使用new将调用相应的类构造函数来初始化新创建的对象
  • 可以使用->运算符通过指针访问类方法:
    if(sayings [i].length()< shortest->length());
  • 可以对对象指针应用解除引用运算符(*)来获取对象。

2.析构函数调用时机

  • 如果对象是动态变量,则当执行完定义该对象的程序时,将调用该对象的析构函数。
  • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时将调用对象的析构函数。
  • 如果对象是用new创建的,则仅当显式使用delete删除对象时,其析构函数才会被调用。
  1. 再谈定位new运算符
    如果使用定位new运算符来为对象分配内存,必须确保其析构函数被调用。delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。解决方案是显式的为使用定位new运算符创建的对象调用析构函数。对于使用定位new创建的对象,应以与创建顺序相反的顺序进行删除。

复习各种技术

重载<<运算符

     ostream & operator<<(ostream & os, const c_name & obj)
     {
         os<<; //display object contents
         return os;
     }

其中c_name是类名。

2.转换函数
要将单个值转换为类类型,需要创建原型如下的类构造函数 :
c_name (type_name value);
其中c_name是类名,type_name是要转换的类型的名称。

要将类转换为其他类型,需要创建原型如下的类成员函数:
operator type_name();
虽然该函数没有声明返回类型,但应返回所需类型的值。

3.其构造函数使用new的类
如果类使用new运算符来分配类成员指向的内存,在设计时应采取一些预防措施:

  • 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete。
  • 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将其设置为空指针。
  • 构造函数中要么使用new[ ],要么使用new,而不能混用。若构造函数使用的new[],则析构函数应使用delete[];若构造函数使用new,则析构函数应使用delete
  • 应定义一个分配内存(而不是将指针指向已有内存)的复制构造函数。
  • 应定义一个重载赋值运算符的类成员函数,其函数定义如下(其中c_pointer是c_name的类成员,类型为指向type_name的指针)。下面示例假设使用new[]来初始化变量c_pointer:
c_name & c_name::operator=(const c_name &cn)
{
    if(this == & cn)
        return * this; //done if self-assignment
    delete [ ] c_pointer;
    //set size number of type_name units to be copied
    c_pointer = new type_name[size];
    //then copy data pointed to by cn.c_pointer to
    //location pointed to by c_pointre
    ……
    return *this;
}

队列模拟

  1. 链表由节点序列构成。每一个节点中都包含要保存到链表中的信息以及一个指向下一个节点的指针。通常,链表最后一个节点中的指针被设置为NULL(或0),在C++11中应使用新增的关键字nullptr
  2. 嵌套结构和类
    在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。
  3. 对于类中const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。成员初始化列表由逗号分隔的初始化列表组成(前面带冒号),它位于参数列表的右括号之后、函数体左括号之前。例如:
           class Queue
           {
            private:
               const int qsize;
            ……
           }
           Queue::Queue(int qs) : qsize(qs) //initialize qsize to qs
           {
               front = rear = NULL;
               items = 0;
           }
            //通常,初值可以是常量或构造函数的参数列表中的参数,等同于:
           Queue::Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0)
           {
            }

注意,只有构造函数可以使用这种初始化列表语法,对于const类成员和被声明为引用的类成员,必须使用这种语法。

  1. 成员初始化列表的语法:如果Classy是一个类,而mem1、mem2和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:
Classy::Classy(int n, int m) : mem1(n), mem2(0), mem3(n*m+2)
{
    //……
}

上述代码将mem1初始化为n,将mem2初始化为0,将mem3初始化为n*m+2.从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。注意:

  • 这种格式只能用于构造函数
  • 必须用这种格式来初始化非静态const数据成员(至少在C++11之前是这样的)
  • 必须用这种格式来初始化引用数据成员
    数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。(警告:不能将成员初始化列表语法用于构造函数之外的其他类方法)。成员初始化列表使用的括号方式也可用于常规初始化。
  1. 当对象被按值传递(或返回)时,复制构造函数将被调用,应遵循优先采用按引用传递对象的惯例。复制构造函数还被用于创建其他的临时对象。
  2. C++逐个对成员进行初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。如果原始对象的成员指向一个数据块,则副本成员将指向同一个数据块。当程序最终删除这两个对象时,类的析构函数将试图删除同一个内存数据块两次,这将出错。解决方法是:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符。
  3. 如果使用了定位new运算符(而不是常规new运算符)为类对象分配内存,则必须负责显式地为该对象调用析构函数,方法是使用指向该对象的指针调用析构函数方法。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhugenmi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值