C++ 类的内存分配是怎么样的?

这篇具有很好参考价值的文章主要介绍了C++ 类的内存分配是怎么样的?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

dynamic_memory

首先通过一段代码来引入动态内存分配的主题。一个名为StringBad的类以及一个功能更强大的String类。

#include<iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_

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

介绍一下这些定义,第一个char指针来表示一段字符串,这就意味着类声明没有为字符串本身分配存储空间。而是要在构造函数里通过new来为字符串分配空间,这就避免了在类声明里面预先定义字符串长度。

num_strings这个东西被声明为了静态存储类,静态成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享一个静态成员,具体解释可以看到下图的代码运行结果。

C++ 类的内存分配是怎么样的?C++ 类的内存分配是怎么样的?

这对于所有类对象都具有相同值的类私有数据都是非常方便的,比如num_strings可以记录所有创建的对象数目。

C++ 类的内存分配是怎么样的?

#include<iostream>
#include<cstring>
/*#include <cstring>:
这是 C 语言标准库中的头文件,提供了一系列操作 C 字符串(字符数组)的函数。
在 C++ 中,<cstring> 头文件中的函数都被放在 std 命名空间中,并且可以使用 C 风格的字符串处理函数,比如 strcpy, strcat, strlen 等。
示例用法:#include <cstring> 可以用来进行基于字符数组的字符串操作,如拷贝、连接、比较等。
#include <string>:
这是 C++ 标准库中的头文件,提供了 std::string 类及相关操作,是 C++ 中用来处理字符串的首选方式。
<string> 头文件中定义了字符串类 std::string,提供了丰富的字符串操作方法,比如字符串拼接、查找、替换等。
示例用法:#include <string> 可以用来定义和操作 C++ 标准库中的字符串对象,避免了使用 C 风格的字符数组所带来的问题。*/
#include "strngbad.h"
using std::cout;
int StringBad::num_strings = 0;

StringBad::StringBad(const char* s) {
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);//使用strcpy函数时,它会从源地址开始拷贝字符,
	//直到遇到字符串结尾的null字符\0为止,所以可以通过指向字符串的指针来操作字符串
	num_strings++;
	cout << num_strings << ": \"" << str
		<< "\" object created\n";
}
StringBad::StringBad() {
	len = 4;
	str = new char[4];
	std::strcpy(str, "c++");
     num_strings++;
	cout << num_strings << ": \"" << str
		<< "\" object created\n";
}
StringBad::~StringBad() {
	cout << "\"" << str << "\" object deleted";
	cout << num_strings << " left \n";
	delete[]str;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st) {
	os << st.str;
	return os;
}

这段代码就是对模板文件的方法进行了定义,首先不能在类声明中初始化静态成员变量,因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类外单独初始化,因为静态类成员是单独存储的,而不是对象的组成部分。

第一个构造函数里,类成员是指针,所以构造函数必须提供内存来存储字符串。然后将字符串复制到内存里。要理解这种方法,必须知道字符串并不保存在对象里,字符串单独保存在堆内存里面,对象仅仅保存了指出到哪里去查找字符串的信息。比如str = s这种语句只保存了地址,而没有创建副本。

在析构函数里面,str指向的是new分配的内存,当stringbad对象过期时,str指针也会过期。但是指向的内存仍然被分配,删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。因此必须使用析构函数。

最后一个重载<<函数就是把 StringBad 对象中的 str 成员变量输出到给定的输出流 os 中,然后返回输出流对象本身。这样做的目的是为了支持链式输出。

#define  _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include "strngbad.h"
using std::cout;
void callme1(StringBad&);
void callme2(StringBad);

int main() {
	using std::endl;
	{
		cout << "Starting an inner block.\n" << endl;
		StringBad headline1("Regina in home");
		StringBad headline2("having sex");
		StringBad sport("Dance");
		cout << "headline1:" << headline1 << endl;
		cout << "headline2:" << headline2 << endl;
		cout << "sport:" << sport << endl;
		callme1(headline1);
		cout << "headline1:" << headline1 << endl;
		callme2(headline2);
		cout << "headline2:" << headline2 << endl;
		cout << "把一个对象初始化给另一个对象:\n";
		StringBad ss = sport;
		cout << "ss:" << ss << endl;
		cout << "用=号进行赋值:\n";
		StringBad regina;
		regina = headline1;
		cout << "regina:" << regina << endl;
	}
	cout << "Exit the main\n";
	return 0;
}
void callme1(StringBad & rsb) {
	cout << "通过引用传递的字符串\n";
	cout << "     \"" << rsb << " \"\n";
}
void callme2(StringBad sb) {
	cout << "通过值传递的字符串\n";
	cout << "     \"" << sb << " \"\n";
}
#define  _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)

首先说第一段代码是有残缺的,这些缺陷使得输出不确定,就像下面的输出红框一样。就是因为strcpy在 StringBad::StringBad(const char* s)StringBad::StringBad() 构造函数中,使用 strcpy 函数来将源字符串复制到 str 缓冲区中,如果源字符串长度超过了 str 缓冲区的大小(len + 1),就会导致缓冲区溢出,可能引起程序崩溃或数据损坏

C++ 类的内存分配是怎么样的?

上述第一个红框里的乱码字符来自通过值传递的字符串的函数callme2,因此在函数执行完毕返回时,会销毁 callme2 函数内部的 headline2 对象,从而触发该对象的析构函数的调用。

通过引用传递参数给函数时,不会触发析构函数的原因在于引用本身并不拥有被引用对象的所有权,它只是对对象的一个别名或者引用。因此,当引用超出其作用域时,不会触发被引用对象的析构函数。

析构函数的调用主要与对象的生命周期和所有权有关。当一个对象的所有权转移或该对象的生命周期结束时,其析构函数会被调用以执行必要的清理操作。然而,通过引用传递并不改变对象的所有权,只是提供了对对象的访问方式,因此不会触发析构函数。

其实程序在运行时已经报错C++ 类的内存分配是怎么样的?

这条消息通常意味着在使用堆内存(通过 newdelete 进行内存分配和释放)时出现了问题,可能是由于内存泄漏、重复释放已释放的内存或者其他与堆内存操作相关的错误导致的。

实际上最后的num_string的值是-2,在《C++ primer Plus》里面其实用Borland C++运行会成这样,我没有运行出来。

image-20240323131833684其实是因为StringBad ss = sports这句没有调用默认的构造函数,也没有调用有参数的构造函数,而是StringBad(const StringBad &)的一种复制构造函数,当用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数,这个函数我们没有声明过,不知道要更新静态变量,所以实际有三次的已知构造函数+2次默认系统构造函数和5次析构函数。

新建一个对象并将其初始化为同类现有对象,复制构造函数都将被调用。最常见的将新对象显式地初始化为现有对象。有下列四种情况:

  1. StringBad regina(ivan);
  2. StringBad regina = ivan;
  3. StringBad regina = StringBad(ivan);
  4. StringBad * regina = new StringBad(ivan);

每当程序生成了对象副本时,编译器都将使用复制构造函数。当函数按值传递对象或函数返回对象时,都将使用复制构造函数。

所以如果按照刚刚的说法,我们显式的定义一下复制构造函数

StringBad::StringBad(const StringBad& st) {
	num_strings++;  // 增加对象计数
}

C++ 类的内存分配是怎么样的?

会发现隐式复制构造函数是按值进行复制,所以原函数里的功能相当于ss.str = sport.str;这里复制的并不是字符串,而是一个指向该字符串的指针。我们重载了运算符<<,并且在析构函数中释放时,ss.str将会被释放,sport所对应的字符串的内存将会消失。但是因为两个对象指向了同一个位置的内存,第二次再调用delete的时候该内存已经没有了,就会导致不确定性,可能会释放掉其他位置。

所以我们将全面的显式定义一下。

StringBad::StringBad(const StringBad& st) {
	len = st.len;  // 复制长度
	str = new char[len + 1];  // 分配新的内存
	std::strcpy(str, st.str);  // 执行复制
	num_strings++;  // 增加对象计数
	std::cout << num_strings << ": \"" << str
		<< "\" object created by copy\n";  // 输出信息
}

代码里还有一个地方是StringBad regina; regina = headline1;这里也调用了复制构造函数,赋值运算符的隐式实现也对成员进行逐个复制,如果成员本身就是对象,则程序将使用为这个类定义的赋值运算符来复制该成员。但是这样做还有问题!也是和上面的问题一样,regina和headline1指向同一个内存位置的str,不能无故删除两次。我们还需要对=符号进行一个重载。

StringBad& StringBad::operator=(const StringBad& st) {
	if (this == &st) {
		return *this;
		/*在赋值操作符重载函数中,
		返回*this意味着返回当前对象的引用。
		这样做的目的是为了支持连续赋值操作,
		比如a = b = c。通过返回对象自身的引用,
		可以实现多重赋值操作的链式调用。*/
	}
	delete[] str;
	len = st.len;
	str = new char[len + 1];  // 分配新的内存
	std::strcpy(str, st.str);  // 执行复制
	return *this;
}

这段代码里面的语法首先是检查自我复制,这个对比的是地址,如果相同就返回本身的引用。如果不相同就和之前的复制操作一样了。

C++ 类的内存分配是怎么样的?

改进后的类

class NewString {
private:
	char* str;
	int len;
	static int num_strings;
	static const int CINLIM = 80;
public:
	NewString(const char* s);
	NewString();
	NewString(const NewString&);
	~NewString();
	int length() const { return len; }

	NewString& operator=(const NewString&);
	NewString& operator=(const char *);
	char& operator[](int i);
	const char& operator[](int i) const;
	friend bool operator<(const NewString& s1, const NewString& s2);
	friend bool operator>(const NewString& s1, const NewString& s2);
	friend bool operator==(const NewString& s1, const NewString& s2);
	friend ostream& operator<< (ostream & os, const NewString & ns);
	friend ostream& operator>> (ostream& is, NewString& ns);
};

这就是我们修改之后的新的类定义。我们可以看到新增了很多的方法以及符号重载,下列我会依次介绍每一种用法。

首先是默认的无参数构造函数

NewString::NewString() {
	len = 0;
	str = new char[1];
	str[0] = '\0';
    num_strings++;
}

为什么不再是之前的写法了,而是要开辟一个数组空间。其实不加[1]两种形式分配的内存量是相同的,区别在于前者和类析构函数兼容,而后者不兼容。析构函数为

NewString::~NewString() {
	--num_strings;
	delete[] str;
}

delete[]和 new[]初始化的指针和空指针都兼容,直接将str写成0 也代表了设置为空指针,和前面的代码功能相同,因为空指针通常用整数0来表示,因为0是一个特殊的地址值,代表着无效的内存地址。在C++11里面,str=nullptr;的写法也同样表示空指针。


friend bool operator<(const NewString& s1, const NewString& s2);
friend bool operator>(const NewString& s1, const NewString& s2);
friend bool operator==(const NewString& s1, const NewString& s2);

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

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

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

把运算符重载写成友元函数,有助于将类对象和普通字符串进行比较。不光是可以访问类的私有静态成员,还可以进行比较,假设regina是一个对象,则

"love" == regina & operator==("love",regina) & operator==(NewString("love"),regina)

都是成立的。


char& operator[](int i);
const char& operator[](int i) const;

实际的需求就是希望获取字符串的某一位时简单一些,我们可以对[]进行重载。

char& NewString::operator[](int i) {
	return str[i];
}

这里使用的是char&,主要目的是允许通过该函数返回的引用来修改调用对象内部存储的字符数组中的元素。当函数返回一个 char& 类型时,它实际上返回的是一个指向字符数组中特定位置的引用。通过返回引用而不是值,可以直接在调用对象的字符数组中进行读写操作,具体区别如下图所示。

C++ 类的内存分配是怎么样的?

C++ 类的内存分配是怎么样的?

const char& operator[](int i) const;但是为什么还有这句话呢,因为在示例里的代码会用到const关键字使const A a("regina");里面的a变成了常量对象,这种对象是无法被修改的,这和我们重载运算符的目的相违背,所以需要特意的写一个常量的重载方法。

const char& NewString::operator[](int i) const{
	return str[i];
}
//第一个 const 关键字放在函数声明或定义的最后表示该函数是一个常量成员函数,即在该函数内部不能修改对象的成员变量。
//第二个 const 关键字放在函数返回类型 char& 前面表示该函数返回一个常量引用,即返回值不能被修改。

可以将成员函数也声明为静态的(函数声明必须包含static关键字,但如果函数定义是独立的,则其中不能包含static)。因为首先不能通过对象调用静态函数,实际上静态成员函数甚至都不能调用this指针。如果静态成员函数在公有部分声明,则可以用类名和作用域解析符调用。(完整代码看最后)

其次由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。所以代码里的Howmany函数只能使用num_strings。其他的私有成员都不能访问。

静态成员函数属于类本身而不是类的实例,因此它们在不依赖于特定对象状态的情况下执行。由于静态成员函数不会自动获取任何类实例的指针或引用,所以它们无法直接访问非静态数据成员或调用非静态成员函数,这些成员和函数都是特定于类的对象的。


C++ 类的内存分配是怎么样的?

这段代码里面有一个delete操作,这个可以不加,但是由于目标对象可能引用了以前分配的数据,一般情况我们需要先释放掉这个引用对象的str指向的内存,来为新字符串分配足够的内存。

代码

newString.h
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#ifndef NEWSTRING_H_
#define NEWSTRING_H_

class NewString {
private:
	char* str;
	int len;
	static int num_strings;
	static const int CINLIM = 80;
public:
	NewString(const char* s);
	NewString();
	NewString(const NewString&);
	~NewString();
	int length() const { return len; }

	NewString& operator=(const NewString&);
	NewString& operator=(const char *);
	char& operator[](int i);
	const char& operator[](int i) const;
	friend bool operator<(const NewString& s1, const NewString& s2);
	friend bool operator>(const NewString& s1, const NewString& s2);
	friend bool operator==(const NewString& s1, const NewString& s2);
	friend ostream& operator<< (ostream & os, const NewString & ns);
	friend istream& operator>> (istream& is, NewString& ns);

	static int HowMany();
};
#endif // !
#Newstring.cpp
#include "newString.h"
#include<cstring>

int NewString::num_strings = 0;
NewString::NewString() {
	len = 0;
	str = new char[1];
	str[0] = '\0';
	num_strings++;
}
NewString::~NewString() {
	--num_strings;
	delete[] str;
}

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

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

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

char& NewString::operator[](int i) {
	return str[i];
}

const char& NewString::operator[](int i) const{
	return str[i];
}

int NewString::HowMany() {
	return num_strings;
}

NewString::NewString(const char* s) {
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);//使用strcpy函数时,它会从源地址开始拷贝字符,
	//直到遇到字符串结尾的null字符\0为止,所以可以通过指向字符串的指针来操作字符串
	num_strings++;
}

NewString::NewString(const NewString& ns) {
	/*区分用=赋值的两个实例指向同一个内存地址*/
	len = ns.len;
	str = new char[len + 1];
	std::strcpy(str, ns.str);
	num_strings++;
}

NewString& NewString::operator=(const NewString& ns) {
	if (this == &ns) {
		return *this;
	}
	delete[] str;
	len = ns.len;
	str = new char[len + 1];
	std::strcpy(str, ns.str);
}

NewString& NewString::operator=(const char * s) {
	/*NewString name;
	char tmp[40];
	cin.getline(tmp,40);
	name = tmp;这个过程会一直创建一个临时对象,
	然后再调用析构函数删除该对象,很低效*/
	delete[] str;
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
    return *this;
}

ostream& operator<<(ostream& os, const NewString& ns) {
	os << ns.str;
	return os;
}

istream& operator>>(istream& is, NewString& ns) {
	char tmp[NewString::CINLIM];
	is.get(tmp, NewString::CINLIM);
	if (is) {
		ns = tmp;
	}
	while (is && is.get() != '\n') {
		continue;
	}
    /*NewString name; cin >> name; 直接可以实现输入为str*/
}

这样代码就完成了对于基本赋值和一些基础运算符的优化,具体实现可以参照《C++ primer plus》里面445页的代码进行实现。文章来源地址https://www.toymoban.com/news/detail-843115.html

到了这里,关于C++ 类的内存分配是怎么样的?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 谷歌的Bard怎么样?

    Bard是什么? ChatGPT: Bard可以指以下几种事物: Bard是一个英雄联盟(League of Legends)游戏中的角色名称,他是一个能够进行攻击和治疗的辅助英雄。 Bard是指中世纪欧洲的一类文学艺术家,主要从事吟游诗人的活动,通过口头传统的方式来讲述历史故事和传说。 Bard是指一种用

    2024年02月10日
    浏览(44)
  • 嵌入式就业怎么样?

    嵌入式就业怎么样? 现在的IT行业,嵌入式是大热门,下面也要来给大家介绍下学习嵌入式之后的发展以及就业怎么样。   首先是好找工作。 嵌入式人才目前是处于供不应求的状态中,据权威统计机构统计在所有软件开发类人才的需求中,对嵌入式工程师的需求达到全部需求量

    2023年04月24日
    浏览(46)
  • 校园跑腿小程序怎么样?

    随着社会的发展和人们生活水平的提高,高等教育越来越受到重视。大学校园不仅是学习的地方,也是学生们日常生活的场所。然而,在繁忙的学业和生活压力下,学生可能经常面临诸如代购、快递、取餐等各种琐碎但繁琐的任务。基于这个需求,校园跑腿小程序应运而生。

    2024年02月16日
    浏览(54)
  • 怎么样设计短链接系统?

    短链接是什么? 短链接,顾名思义就是短的链接,能将很长的链接转化为短链接,并且能由短链接指向原来长链接所指向的地方。 为什么需要短链接? 在很多地方,比如微博、推特限制了每一条博文内容长度,那么如果链接太长可能就无法发出去了。 生成的二维码也更加容

    2024年02月11日
    浏览(48)
  • 成都北大青鸟怎么样?

    对于任何一个培训机构的了解大概的流程是:听说名字——网上搜索,可以看到机构官网,也会看到机构广告,当然也会看到各种有好有坏的评论,到这里会对机构形成初印象;然后如果身边有培训出身的小伙伴会去询问对机构有没有了解等等。 然后就开始考虑线下实地考察

    2023年04月08日
    浏览(41)
  • 电脑恢复出厂设置会怎么样

    电脑恢复出厂设置会怎么样大家知道吗?在这里小编我想告诉你的是,如果您想把当前的电脑系统重置,那么您电脑桌面以及 C 盘里的资料就会被成功删除掉了.今天来看看电脑恢复出厂设置会怎么样以及电脑恢复出厂设置的方法的介绍。 工具/原料: 系统版本:Windows10系统 品牌

    2024年02月13日
    浏览(48)
  • 百度文心一言怎么样?

    文心一言在3月16号发布,刚发布时只有邀请码体验,没有申请体验通道。到了晚上看到申请体验通道,便提交了申请,到4月1号过了半个月终于收到短信可以体验了。   本来想看看接口,也没什么恶意,但是却遭到了拒绝,感觉是小心翼翼,生怕泄露了什么。 另外,满屏的

    2023年04月08日
    浏览(41)
  • 小发猫降aigc怎么样?

    AIGC检测上升,新一代软件助力降低AI生成论文的识别率,下面分享小发猫降aigc怎么样? 随着科技的不断进步,人工智能(AI)在学术界的运用日益广泛,AI生成的论文数量呈现上升趋势。然而,这也导致了AIGC(人工智能生成内容检测)的识别率不断攀升,引起了学术界的广泛

    2024年03月15日
    浏览(86)
  • node怎么样安装npm

    怎么样安装npm? 1、官网下载: https://nodejs.org/en/download/ 2、安装完成后CMD命令窗口输入“node -v”或“node --version”命令 出现版本信息: 3、npm默认的仓库地址是在国外网站,速度较慢,建议大家设置到淘宝镜像。但是切换镜像是比较麻烦的。 4、推荐一款切换镜像的工具:n

    2024年02月04日
    浏览(43)
  • 网络安全就业形势怎么样?

    泻药,以下都是我本人的肺腑之言,是答主深耕职场多年,转战数家公司+总结周围朋友的从业经验才总结出来的行业真相,真心希望帮助到还没有步入职场的大家,尤其是24届的应届毕业生,多掌握些就业信息就能少走一些弯路,互联网公司、央国企、公务员……哪个赛道不

    2024年02月07日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包