C++:动态内存和类(第十二章)

例子1:创建一个StringBad类,成员有指向char数组的指针和字符串长度int,以及一个存储字符串数的静态变量。

stringbad.h

#ifndef STRINGBAD_H_
#define STRINGBAD_H_
#include<iostream>

class StringBad{
    private:
        char * str;
        int len;
        static int num_str;
    public:
        StringBad();
        StringBad(const char * c);
        ~StringBad();
        friend std::ostream& operator<<(std::ostream& os,const StringBad& s);
};

#endif

1.cpp

#include"stringbad.h"
#include<cstring>
using std::cout;

int StringBad::num_str =0;

StringBad::StringBad(){
    str = new char[4];
    len =3;
    strcpy(str,"C++");
    num_str++;
    cout<<num_str<<": \""<<str<<"\" default object created\n";
}

StringBad::StringBad(const char* c)
{
    len = strlen(c);
    str = new char[len+1];
    strcpy(str,c);
    num_str++;
    cout<<num_str<<": \""<<str<<"\" default object created\n";
}

StringBad::~StringBad(){
    cout<<"\""<<str<<"\" deleted, ";
    num_str--;
    delete [] str;
    cout<<num_str<<"left\n";
}

std::ostream& operator<<(std::ostream& os,const StringBad& s)
{
    os<<s.str;
    return os;
}

程序解析:

  1. 构造函数以及default构造函数用new来为字符串指针分配大小为len+1的动态内存(因为strlen()得到的是字符串的长度,not include '\0')
  2. 然后用strcpy()来赋值,将字符串复制到新的内存中,字符串并不保存在对象中,而是保存到heap中,对象仅仅是指出该地址在哪。
  3. 直接用等号的话是不可以的,因为这是指针,str = s只保存了地址,没有创建字符串副本。
  4. 不能在类声明中初始化静态变量,因为类声明是描述如何分配内存,而不分配内存。对于静态类成员,可以在类声明之外使用单独语句初始化,因为静态类成员是单独存储的,不在哪个对象里,是共享的。如果在头文件初始化,且在多个文件里引用头文件,将多次初始化,发生错误。但静态数据成员的const整数类和枚举类是例外,可以类声明初始化。
  5. 析构函数里必须包括delete [] str,以及num_str--;
    StringBad过期时,str指针也将过期,但str指向的内存仍然被分配。所以必须用delete释放该内存。

2.cpp(将发生很多错误)

#include"stringbad.h" 
using std::cout;
void func1(StringBad& s);
void func2(StringBad s);
int main()
{
    
    using std::endl;
    {
        StringBad s1("I am Jeff11111");
        StringBad s2("I am Jeff22222");
        StringBad s3("I am Jeff33333");
        cout<<"s1 : "<<s1<<endl;
        cout<<"s2 : "<<s2<<endl;
        cout<<"s3 : "<<s3<<endl;
        func1(s1);
        cout<<"s1: "<<s1<<endl;
        func2(s2);
        cout<<"s2: "<<s2<<endl;
        cout<<"\n 初始化一个新的对象\n";
        StringBad s4 = s3;
        cout<<"s4 : "<<s4<<endl;
        cout<<"s3 : "<<s3<<endl;
        cout<<"\n 赋值一个对象到一个新对象\n";
        StringBad s5;
        cout<<"赋值之前 s5 : "<<s5<<endl;
        s5 = s1;
        cout<<"赋值之后 s5 : "<<s5<<endl;
        cout<<"此时s2 : "<<s2<<endl;
        cout<<"离开代码块: \n";
    }
    cout<<"Over!\n" ;
    return 0;
}
image.png

程序解析:

  1. 按值传递的函数调用之后,接着就是析构函数的调用。
  2. s2指向的字符串被篡改
  3. 初始化一个新对象没有构造函数里要打印的信息。
  4. s2指向的字符串内容变成C++
  5. 离开代码块之后,应该会调用5次析构函数,但仅仅调用了3次,且第三个既s3还出现乱码,连num_str都没了。

所以问题出现在:
1.用对象来初始化一个新对象没有使用构造函数
2.按值传递函数,将调用析构函数
3.s2所指向的字符串内存地址,被删除了,然后被s5默认构造函数创建的那个"C++"占领了
4.赋值s5=s1并没有把s2的"c++"改掉。

特殊成员函数

当没有相关设计时,C++将提供下面这个成员函数:
1.默认构造函数
5.默认析构函数
2.复制构造函数
3.赋值运算符
4.地址运算符

复制构造函数

  1. 复制构造函数用于将一个对象复制到新创建的对象中,用于初始化过程中,而不是常规赋值过程中。
    原型class_name(const Class-name& )
  2. 何时被调用?
    新建一个对象并将其初始化为同类现有对象时,将会出现。比如:
StringBad ditto(motto)
StringBad metto = motto;
StringBad also = StringBad(motto);
StringBad* ptr = new StringBad(motto);

其中中间两种情况,可能是直接用复制构造函数创建新对象,或者是复制构造函数创建一个motto对象副本,然后赋值给新对象。最后一种声明使用motto初始化一个匿名对象,然后将新对象地址传递给ptr

  1. 按值传递函数意味着创建原始变量的一个副本,编译器生成临时变量,使用复制构造函数。然后结束函数时,将销毁副本。就调用了析构函数,由于副本是复制原始变量的值,所以原始变量指针指向的地址就同副本指针指向地址一致,所以改地址会被销毁,而且num_str也会被减一。
  2. default复制构造函数的功能
    就是逐个复制非静态成员(因为静态成员不是对象里的成员,而是整个类的成员)
  3. 解决方法:
    1.避免使用这种方法
    2.重新定义一个显式复制构造函数

赋值运算符

  1. 原型如下:
    class_name& class_name::operator=(const class_name &)
  2. 何时使用?
    将已有的对象赋值给另一个对象时,将使用。但初始化时,一般都是自动使用复制构造函数。
  3. 赋值运算符也是将非静态成员逐个赋值,然后初始化s5的时候,s2指针所指向的内存地址其实已经被delete了,所以是个空出来的位置。所以s5初始化就指向这个位置(原来是s2的指针所指),后来赋值s5 = s1,s5的指针就指向s1指针所指的地方。所以"C++"的地址的内容并没有被改变。也是成员复制问题。

修改地方如下:

复制构造函数
StringBad::StringBad(const StringBad& s)
{
    len = s.len;
    str = new char[len+1];
    strcpy(str,s.str);
    num_str++;
    cout<<num_str<<": \""<<str<<"\" default object created\n";
}

赋值运算符
StringBad& StringBad::operator=(const StringBad& s)
{
    if(this == &s)
        return *this;
    delete [] str;
    len = s.len;
    str = new char[len+1];
    strcpy(str,s.str);
    return *this;
}

程序解析:

  1. 复制构造函数
    1.它的参数是类的引用(如果是按值还要创建副本效率低,而且类的引用,主程序按值传递也适用)
  2. 赋值运算符
    1.首先返回的是类的引用(因为s5=s1,懂了吧)
    2.然后判断该地址是不是本身(用隐式地址==&s)
    3.不是的话要先删除原本的地址,然后再重新分配。
image.png

(下一节内容....)

这一节要往类里面加入的功能:

int length()  const {return len}
friend bool operator<(const StringBad& s1,const StringBad& s2);
friend bool operator==(const StringBad& s1,const StringBad& s2);
friend bool operator>(const StringBad& s1,const StringBad& s2);
friend istream& std::operator>>(std::istream& is, StringBad& s);
StringBad& operator=(const char * s);
char& operator[](int n);
const char& operator[](int n) const;
static int Howmany();

修改后的默认函数

StringBad::StringBad(){
    len = 0;
    str = new char[1] ;
    str[0] = nullptr ; 
    num_str++;
}

1.用str = new char[1]而不用new char是因为要对照析构函数里的delete
2.用nullptr来表示空指针(仅支持C+11版本,可以NULL或0)

比较函数

可以用strcmp(S1,S2)来返回bool值。当第一个参数小于第二个参数时,返回的是一个负值。等于时返回0。大于时返回一个正值。

bool operator<(const StringBad& s1,const StringBad& s2)
{
    return (std::strcmp(s1.str,s2.str)<0);
}

bool operator>(const StringBad& s1,const StringBad& s2)
{
    return s2<s1;
}

bool operator==(const StringBad& s1,const StringBad& s2)
{
    return (std::strcmp(s1.str,s2.str)== 0);
}
  1. 用<运算符来表示>运算符,对于内联函数是个很好的选择。
  2. 将比较函数用作友元函数,有助于string对象与常规C字符串进行比较。如if(“loving”== str) 【str为类对象】

中括号表示法访问字符

char& StringBad::operator[](int n)
{
    return str[n];
}

1.假设opr是一个类对象,opr[4]就被转换为opr.operator,将访问第五个字符。
2.还可以opr[5] = 'r',转换为opr.operator = 'r' 。这样代码访问了私有成员,但由于类方法,所以可以访问。
3.如果有个const常量, 上面方法不确保不会修改,所以另外定义一个函数const char& operator[](int n) const。C++将根据常量和非常量的特征标来区分函数。

const char& StringBad::operator[](int n) const
{
    return str[n];
}

静态成员函数

  1. 可以将成员函数声明为静态,则有两个后果
    1.不能通过对象调用静态成员函数。静态成员函数不能使用this指针。如果静态成员函数声明在公有部分,则可以使用类名加作用域解析运算符来使用它。方法在类声明中声明和定义如下:
    static int Howmany() { return num_str;}
    2.静态成员函数可以访问静态成员,但不能访问其他私有成员,因为不与特定对象相关联。

重载赋值运算符
若有如下代码:

StringBad s;
char temp[40];
cin.getline(temp,40);
s = temp;

最后一句工作原理:
1.使用构造函数StringBad(const char * )来创建一个临时对象,其中包括temp字符串副本。
2.使用之前声明的StringBad& operator=(const StringBad& )来进行复制。
3.析构函数删除临时对象。
这样效率太低了,所以可以直接重载赋值运算符
数组名是char*

StringBad& StringBad::operator=(const char* s)
{
    delete [] str;
    len = std::strlen(s);
    str = new char[ len +1];
    std::strcpy(str,s);
    return *this;
}

重载>>运算符

1.同重载<<运算符一样,要返回istream类引用。假定输入不得超过最大字数CINLIM(在private部分定义:static const int CINLIM = 80;)
2.if条件下,如果文件到达末尾或读取的是空行,导致输入失败,is将设置为false

std::istream& operator<<(std::istream& is, StringBad& s)
{
    char temp[StringBad::CINLIM];
    is.get(temp,StringBad::CINLIM);
    if(is)
        st = temp;
    while(is && is.get() !='\n')
        continue;
    return is;
}

程序设计
该程序允许输入几个字符串,然后存储到StringBad对象中,并显示他们,并指出哪个字符串最短,哪个字符串按字母顺序排在最前。

#include"stringbad.h" 
using std::cout;
using std::endl;
using std::cin;
const int ArSize = 10;
const int MaxLen = 81;
int main()
{
    StringBad name;
    cout<<"What's your name?"<<endl;
    cin>>name;
    cout<<name<<"输入不超过"<<ArSize<<"句的短句(空行退出): "<<endl;
    StringBad sayings[ArSize];
    char temp[MaxLen];
    int i;
    for(i=0;i<ArSize;i++)
    {
        cout<<i+1<<": ";
        cin.get(temp,MaxLen);
        while(cin&&cin.get()!='\n')
            continue;
        if(!cin||temp[0]=='\0') 
            break;
        else
            sayings[i] = temp;
     } 
     int total = i;
     if(total>0)
     {
        cout<<"Here is your saying: "<<endl;
        for(i=0;i<total;i++)
        {
            cout<< sayings[i][0]<<": "<<sayings[i]<<endl;
            cout<<"length; "<<sayings[i].length()<<endl;
         }
         
        int shortest =0;
        int first =0;
        for(i=1;i<total;i++)
        {
            if(sayings[i].length()<sayings[shortest].length())
                shortest = i;
            if(sayings[i]< sayings[first])
                first = i;
        }
        
        cout<<"Shortest: "<<sayings[shortest]<<endl;
        cout<<"First: "<<sayings[first]<<endl;
        cout<<"There are "<<StringBad::Howmany()<<" StringBad object"<<endl;
     }
     else
        cout<<"no sayings\n";
    return 0;
}

程序解析:

  1. 使用了类对象
  2. if(!cin || temp[0]=='\0')输入失败或空行会退出循环

构造函数使用new注意事项:

  1. 如果在构造函数使用new来初始化指针,则在析构函数使用delete
  2. new和delete要对应。new对应delete,new [] 对应delete []
  3. 如果有多个构造函数,必须以相同的方式使用new,因为只有一个析构函数。默认构造函数将它设置为nullptr或0
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351