首页 > 编程语言 >C++ 对象

C++ 对象

时间:2024-04-24 17:22:20浏览次数:29  
标签:const name 对象 函数 C++ Person string 构造函数

概述

C++的招牌能力之一,也是C++的核心特性没有之一, 也是在 C 基础扩展的最重要的能力,一切皆可封装为对象,有三大主要特性,封装、多态、继承。

基础

简单理解,类就是用户自定义的一种数据结构,封装了数据和行为(函数)的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象,类定义格式如下。

class ClassName {
    Access Specifiers:		// 访问修饰符 public、private、
        type VarName;		// 成员变量
        type function(){}	// 成员函数
}

定义名称为 Person的类,包含一个成员变量、成员函数

class Person {
public:
    string name;

    void say() {
        std::cout << "Im " << this->name << endl;
    }
}

上面示例,在类的内部使用了this指针访问成员属性name,也可以使用作用域解析运算符::,调整 say 函数代码如下,效果相同

    void say() {
        std::cout << "Im " << Person::name << endl;
    }

然后,以该类为模板创建对象实体

Person p;
p.name = "tom";
p.say();

和声明普通变量类似,创建了对象p,同时修改了 name 属性和调用 say 函数,访问成员使用.点运算符。

通过指针方式访问成员属性

Person *ptr = &p;

// 两种方式都可以
(*ptr).say();
ptr->say();

上面示例中,两种方能等价,(*ptr).say()不能写成*ptr.say();,因为点运算符.的优先级高于*,这种写法会将t.age看成一个指针,然后取它对应的值,会出现无法预料的结果,因为写法很麻烦C 语言引入新的箭头运算符->,在 C++中得到沿用,并扩大到对象属性

权限修饰符可控制成员的可见范围,C++支持三种修饰符,分别是:

  • public,公开属性,对象内部、外部都可以访问和修改
  • private,私有属性,默认属性,仅对象内部可以访问和修改
  • protected,受保护属性,对象内部、子类可以访问和修改

简单示例

class Person {
public:
    string name;
private:
    int age;
protected:
    string address;
}

看起来类和结构体定义比较相似,的确两者部分特性相同,但有本质区别,简单总结如下

  • 两者都是自定义复合数据结构,可组合数据和方法
  • 结构体的属性默认是 public且不允许修改,类属性默认是private并支持修改
  • 类支持继承,结构体不支持继承
  • 类支持构造函数、析构函数,结构体不支持
  • 类提供了封装机制,可以隐藏内部实现,只暴露必要的接口;结构体通常用于存储数据,不太注重封装。
  • 类支持运算符重载,结构体不支持
  • 类支持模板类,结构体不支持

总结,类和结构体在C++中都用于定义自定义数据类型,但类更注重封装、继承和多态,而结构体更注重存储数据。

构造函数

构造函数是一种特殊的成员函数,在创建对象时自动执行,主要用于初始化对象,从类型可以分为:无参构造函数、有参构造函数、拷贝构造函数。构造函数的定义也有所区别,首先函数名必须和类名称相同,并且没有返回值(注意void都不需要),不能手动调用,支持初始化列表,支持重载。

构造函数示例,分别定义了三种类型的构造函数

class Person {
public:
    string name;

    Person() {					// 无参构造函数
        this->name = "tom";		
    }
    Person(string name) {		// 有参构造函数
        this->name = name;
    }
    Person(Person const &p) {	// 拷贝构造函数
        this->name = p.name;
    }
}

创建对象,根据参数自动匹配对应的构造函数,并调用执行

Person p;			// 调用无参构造函数
Person p1("jerry");	// 调用有参构造函数
Person p2(p1);		// 调用拷贝构造函数

特别注意,调用无惨构造函数时候没有小括号 (),和函数申明语法冲突了,两者格式一样产生二义性,编译器无法识别是函数申明、还是调用构造函数。

C++规范所有的类有两个默认构造函数,由系统隐式提供,分别是无惨构造函数、拷贝构造函数。默认的拷贝构造函数,自动拷贝所有的属性到新对象上,使用浅拷贝,可以重载实现深拷贝。

class Person {
public:
    string name;
    Addr *p_addr;					// 增加一个指针类型成员变量

    Person(Person const &p) {		// 重载拷贝函数
        this->name = p.name;
        this->p_addr = new Addr(*p.p_addr);		// 深度拷贝,在堆申请内存并创建新对象,记得在析构函数中释放堆内存
    }
}

如果重载了构造函数,系统有可能不再提供默认构造函数,遵循如下规则:

  • 重载有参构造函数,系统就不再提供默认无惨构造函数
  • 重载拷贝构造函数,系统就不再提供默认无惨构造函数、拷贝构造函数。

一般情况如果重载了构造函数,会显示再添加一个无参构造,如下示例

class Person {
public:
    Person(string name) {		// 重载有参构造函数
        ...
    }
    Persion() = default;		// 使用C++11默认构造函数特性
}

C++支持多种方式调用构造函数,也就是多种不同的方式创建对象,Java程序员可能感到惊讶,分别有如下三种
括号法
和申明变量的格式一样,申请内存,创建对象,调用对应构造函数初始化对象

Person p;				// 调用无参构造函数
Person p1("jerry");		// 调用有参构造函数
Person p2(p1);			// 调用拷贝构造函数

显示法
有点类似函数调用,返回匿名对象

Person p = Person()				// 调用无参构造函数
Person p1 = Person("jerry");	// 调用有参构造函数
Person p2 = Person(p1);			// 调用拷贝构造函数

隐式法
最诡异的调用方式,看起来是给变量赋值。单参数的构造函数有个默认隐藏技能,类型转换操作符,当等号右边的类型恰好匹配构造的参数类型,就会调用对应构造函数。

Person p1 = "jerry";	// 调用有参构造函数
Person p2 = p1;			// 调用拷贝构造函数

上面示例中两个语句都触发类型转换运算符,会调用对应的构造函数。特别是p2 = p1语句,看起来p2等于p1赋值语句,其实底层调用拷贝构造函数,两者是完全独立的对象。

这个技能看起来很酷,但在某些情况下容易产生困扰,C++提供了一个修饰符 explicit,被修饰的构造器关闭类型转换操作符的功能

class Person {
public:
    string name;
    explicit Person(Person const &p) {		// 修饰, 关闭类型转换
        this->name = p.name;
    }
}

隐式调用创建对象

// 创建对象
Person p;
Person p1 = p;		// 编译失败

简单总结,使用哪种方式都可以,要统一风格。

调用方法 无参构造 有参构造 拷贝构造
括号法 有限支持 支持 支持
显示法 支持 支持 支持
隐式法 不支持 有限支持 支持

使用new创建对象,任何创建对象的方式前都可以加new关键字,会改变对象存储的位置,将存储在堆内存中,创建过程:申请堆内存,创建对象,返回指针

Person *p = new Person("jerry");
if (p == NULL) {
    exit(-1);
}

p->say();
delete p;

示例中,创建的对象就存储在堆内存,如果分配失败则退出程序,程序最后释放堆内存。

初始化列表,构造函数的特有技能,可在函数定义中增加增加初始化信息,在函数执行前就完成对象属性的初始化

class Person {
public:
    string name;

    explicit Person(const string &new_name) : name(new_name) {}
}

上面示例,函数定义中的: name(name)就是初始化列表,在函数执行前,使用实参new_name初始化对象的name属性,函数逻辑为空,也完成了赋值初始化,当然可以在函数继续修改。

初始化参数列表,还可以用于默认值初始化

class Person {
public:
    string name;

    Person() : name("tom") {}	// 构造函数
}

上面示例,在无参构造函数上增加了初始化列表,参数是固定的 tom,只要无参构造函数创建对象name属性总是tom

析构函数

与构造函数相对应的析构函数,对象销毁时自动执行,主要用于释放资源,如堆内存、文件描述符等,函数定义也有特定格式,函数名称是类名前加波浪号、没有返回值(注意void都不需要)、不能有参数、不支持重载

class Person {
public:
    string name;

    Person() : name(new_name) {}	// 构造函数

    ~Person() {		// 析构函数
        ...
    }
}

对象销毁收会自动执行析构函数,如果对象在栈存储则退出栈时候执行,如果在堆存储则释放内存时候执行。

函数传参

对象也可以做为函数参数传递,注意:和结构体特性一样是值传递,形参和实参是不相等,也就是说在传递过程中会发生对象拷贝,Java程序员可能又会感到惊讶。

定义了一个函数,形参是Person类型

void match_name(Person person) {
    if (person.name == "tom") {
        // ...
    }
}

调用该函数,注意:此时会触发对象拷贝,创建的p和传入的p是两个独立对象,底层是调用对象的拷贝构造函数实现。

Person p("tom");
metch_name(p);

函数的返回值如果是对象,也会触发对象拷贝,如下示例

Addr match_name(Person person) {		// 返回值是addr对象
    if (person.name == "tom") {
        Addr *addr = person.p_addr;
        return *addr;
    }
}

调用该函数,也会触发函数拷贝,返回的addr和接收addr是两个独立的对象,底层也调用 addr拷贝构造函数实现

Person p("tom");
Addr addr = match_name(p);

这种特性并不友好,大多是情况下都是希望传递对象自身,而不是拷贝后的新对象,可能是沿用了 C 语言的结构体特性,C++的对象是在结构体基础扩充而来的。解决方案有两种,分别是指针传递和引用传递,其实两者本质一样,都是地址传递,引用传递简化了语法,更加推荐引用传递方式。下面是两种传递方式示例。

使用指针传递

void match_name(Person const *person) {		
    if (person->name == "tom") {
        ...
    }
}

调用

Person p("tom");
Addr addr = match_name(&p);

使用引用传递,使用更加简洁

void match_name(Person const &person) {
    if (person.name == "tom") {
        ...
    }
}

调用

Person p("tom");
Addr addr = match_name(p);

特别注意的是返回值,如果使用指针或引用返回对象类型,一定要确保指针不能指向局部变量,局部变量存储在栈中,函数调用结束就随之销毁了,返回的地址就是野指针

Addr* match_name(Person const &person) {		
    return person.p_addr;				// ok

    // ok, 指向堆内存
    // return new Addr(*(person.p_addr));	

    // err, 指向局部变量了,野指针
    // Addr addr(*(person.p_addr));
    // return &addr;						
}

this 指针

这是个比较特殊的成员变量,由系统默认提供,它是一个指针,总是指向当前的对象实例。和 Java 的 this、python 的 self 等功能类似

简单示例

class Person {
public:
    string name;

    explicit Person(string const &name) : name(name) {}

    void say() {
        cout << "Im " << this->name << endl;
    }
};

创建两个对象,并执行 say 函数

Person p1("tom");
p1.say();			// Im tom

Person p2("jerry");
p2.say();			// Im Jerry

相同的语句this->name读取内容不一样,因为this总是指向当前的对象

代码调整下,把类定义中的this->name调整为 name,然后看看效果

void say() {
    cout << "Im " << name << endl;
}

和上面的示例一样,创建两个对象,然后执行 say

Person p1("tom");
p1.say();			// Im tom

Person p2("jerry");
p2.say();			// Im Jerry

可以发现两者完完全一样。这个读取变量优先级有关系:局部变量->对象变量->全局变量,逐层向上查找,如果是对象变量,系统会自动补齐this指针。

如果对象属性和局部变量同名时,又想访问对象的成员变量,就可以使用this指针,精确控制读取位置

class Person {
private:
    string name;
public:
    void setName(string const &name) {
        this->name = name;			// this指针
    }
};

如上示例,使用this指针,把局部变量name赋值给对象变量name。也可以总显示的使用this指针,代码指向更加清晰。

还有个重要作用,如果成员函数希望返回对象本身,就可以使用this

class Person {
private:
    string name;
    int age;
public:
    Person* setName(string const &name) {
        this->name = name;
        return this;
    }

    Person* setAge(int const &age) {
        this->age = age;
        return this;
    }
};

使用示例,有点类似 Java 常用的Build技能

Person p;
p1.setName("tom").setAge(10);

另外特别注意, 指针this是被 const修饰过的指针常量,也就是说不允许修改的指向位置,但是可以修改指向的值。声明this的伪代码如下

Person * const this;

如下示例,如果修改this指向将编译失败

void setName(string const &name) {
    this = NULL:		// err 
}

const 修饰

const修饰的变量为只读变量,也可以用于修饰类的多个位置,分别有不同的功能,逐一介绍

修饰成员属性,可称为常属性,就算是 public 的成员属性,只要被修饰都变成只读,不可修改

class Person {
public:
    int age;
    string const name;		// 修饰
    void setName(string const &name) {
        this->name = name;	// err, 编译失败
    }
}

修饰成员函数,可称为常函数,被修饰的成员函数,不允许修改对象自身的任何属性。

class Person {
public:
    string name;
    void setName(string const &name) const {		// 修饰
        this->name = name;	// err, 编译失败
    }
}

以上示例,修饰后的成员函数不允许修改成员属性,另外注意const的位置,是在函数小括号之后,这是个专用语法。

修饰成员函数,其本质是进一步修饰this指针,指向的值也不能修改了,修饰后的this声明伪代码如下

const Person * const this;

所以被const修饰的成员函数,可以读取任意属性,但是不能修改任何属性。

但是 C++有开又增加mutable修饰符,被修饰的成员属性,在常函数中也允许修改,可以更精细的控制权限。

class Person {
public:
    int age;
    mutable string name;

    void setName(string const &name) const {
        this->name = name;		// ok
    }
    void setAge(int const &age) const {
        this->age = age;		// err, 编译失败
    }
    void say() {
        cout << "Im " << this->name << endl;
    }
}

如上示例,name属性被mutable修饰了,所以name属性允许在常函数中修改;age属性则不允许被修改。

修饰对象,可称为常对象,被修饰后不允许修改对象的任何属性,被mutable的除外

const Person p;		// const 修饰
p.name = "tom";		// ok
p.age = 10;			// err

另外注意,常对象只允许调用常函数,下面示例编译失败

p.setNmae("ok");		// ok, setNmae是常函数
p.say();				// err, 普通不允许调用

静态成员

使用 static 关键字定义的是静态属性,静态成员无论创建多少个类的对象,静态成员都只有一个副本。

class Person {
private
    static string category;	// 静态属性
    string getCategory() {		// 静态函数
        category;
    }
public:
    string name;
}

重载运算符

标签:const,name,对象,函数,C++,Person,string,构造函数
From: https://www.cnblogs.com/asdfzxv/p/18155918

相关文章

  • 使用VS Code和WSL开发C/C++的简单配置
    使用VSCode和WSL开发C/C++的简单配置目录使用VSCode和WSL开发C/C++的简单配置使用情形VSCodeDebug简要介绍由于微软文档写的非常详细,感觉没什么写的必要了,后续只贴参数和链接了task配置launch配置C/C++配置参考来源使用情形TheWindowsSubsystemforLinux适用于Linux的W......
  • C++ 指针变量的字面量以及其所指对象的字面量
    指针变量的字面量以及其所指对象的字面量 #include<iostream>usingnamespacestd;intmain(){intvar=20;int*var_address;var_address=&var;cout<<"Valueofvarvariavle:"<<var<<endl;//Valueofvarvari......
  • C++ 指针变量的字面量以及其所指对象的字面量
    指针变量的字面量以及其所指对象的字面量 #include<iostream>usingnamespacestd;intmain(){intvar=20;int*var_address;var_address=&var;cout<<"Valueofvarvariavle:"<<var<<endl;//Valueofvarvari......
  • c++
     如何验证gcc正常使用,编译c以及运行过程要验证GCC(GNUCompilerCollection)是否正常使用,您可以按照以下步骤进行操作:检查GCC是否安装:打开终端或命令行界面,输入以下命令来检查GCC是否已安装:gcc--version 如果GCC已正确安装,您将看到GCC的版本信息。如果没有安装,您......
  • C++ 指针变量的字面量以及其所指对象的字面量
    指针变量的字面量以及其所指对象的字面量 #include<iostream>usingnamespacestd;intmain(){intvar=20;int*var_address;var_address=&var;cout<<"Valueofvarvariavle:"<<var<<endl;//Valueofvarvari......
  • 为什么自动驾驶领域发论文都是用强化学习算法,但是实际公司里却没有一家使用强化学习算
    为什么自动驾驶领域发论文都是用强化学习算法,但是实际公司里却没有一家使用强化学习算法?——(特斯拉今年年初宣布推出实际上第一款纯端到端的自动驾驶系统,全部使用强化算法,替换掉原有的30万行C++的rule-based代码)给出一个自己比较认可的答案:https://www.zhihu.com/question/54......
  • C++ 指针变量的字面量以及其所指对象的字面量
     指针变量的字面量以及其所指对象的字面量 #include<iostream>usingnamespacestd;intmain(){intvar=20;int*var_address;var_address=&var;cout<<"Valueofvarvariavle:"<<var<<endl;//Valueofvarv......
  • 深度解读《深度探索C++对象模型》之C++虚函数实现分析(二)
    接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。第一篇请从这里阅读:深度解读《深度探索C++对象模型》之C++虚函数实现分析(一)这一篇主要讲解多重继承情况下的虚函数实现分析。在多重......
  • jackson.dataformat.xml 反序列化 对象中包含泛型
    重点:@JacksonXmlPropertylocalName指定本地名称@JacksonXmlRootElementlocalName指定root的根路径的名称,默认值为类名@JsonIgnoreProperties(ignoreUnknown=true)这个注解写在类上,用来忽略在xml中有的属性但是在类中没有的情况@JacksonXmlElementWrapper(useWrapping(def......
  • python将字典转换为对象(type的高级用法)
    创建对象使用type()函数还可以动态创建对象。在Python中,对象本质上也是一种类型,因此可以使用type()函数来创建对象。例如:obj=type('MyObject',(),{'attr':'value'})()等价于classMyObject:attr='value'obj=MyObject()创建类使用type()函数还可以......