9.结构体、类、对象


结构体

声明、定义

  • 同一结构体内的成员名不能同名,但是可以和结构体以外的名字同名
  • 结构体声明方法
1
2
3
4
5
struct student{
int age;
char name[10];
……
};
  • 声明的位置决定了能在哪里使用这个结构体,就跟之前的变量一个道理。

  • 声明可以不带具体的成员,但是那样的声明后面只能使用这个结构体的整体,里面的成员是不能操作的,所以比较鸡肋。

  • 不同源文件的结构体本质上是不同的结构体,所以如果需要让同一个结构体在多个源文件同步使用同步修改,要写头文件。

  • 结构体变量定义的时候,C语言不能省去struct修饰:struct student s1;,在C++里面可以省去struct修饰:student s1;

  • 结构体变量可以在声明的同时定义。这样的东西不能放到头文件里去,否则导致出现全局变量。

1
2
3
4
5
struct student{
int age;
char name[10];
……
} s1, s_list[10], *p_s3;
  • 结构体可以不声明名字,一次性使用结构。这样的方式会让以后不能再用这个方式定义这样的结构体了。
1
2
3
4
5
struct {
int age;
char name[10];
……
} s1, s_list[10], *p_s3;
  • 结构体变量初始化,也是大括号,按照声明里的顺序一个个初始化下去
1
struct student s1 = {19, "名字", ……};

使用

  • 使用成员变量,使用成员运算符.s1.name使用起来和普通变量没区别
  • 结构变量允许整体赋值的操作。s1 = {19, "名字", ……};即使含有字符数组的成员也不影响。

存储

  • 内存对齐:基本数据类型变量在内存中的存储满足,起始地址编号是min(类型大小,内存数据宽度)的整数倍。内存数据宽度就是64位32位那个,这里是多少个字节
  • 结构体成员对齐:
    • 结构体变量的起始地址编号要满足是min(max(所有基本类型的大小),内存宽度大小)的倍数
    • 一个结构体变量占据的空间总和,必须是max(所有基本类型的大小)的整数倍。因此在最后可能会有用于填充的字节
    • 每一个数据成员相对于结构体变量起始地址的偏移量必须是这个成员类型大小的整数倍。因此成员间会有填充字节。

指针

  • 指向结构体变量的指针,基类型是这个结构体,所以进行相关计算的时候,是按照一个结构体变量占据的空间总和为单位进行变化的。
  • 间接成员运算符,把从指向结构体变量的指针到这个结构体变量下的一个成员的获取过程简化为这个运算符了。s1->age等效于(*s1).age

类和对象

  • 带有函数操作、访问控制的结构体
  • C没有,C++有
  • 原来的结构体特性都继承到了类上面。
  • sizeof看大小的时候,成员函数不占空间
  • 本质上,class就是struct,struct在C++下也能使用新特性,只是要专门搞一个关键字出来表示重大区别。

新特性

访问控制

  • 使用private: public:成员访问限定符来决定之后的成员函数和数据成员的可访问性。
  • 成员函数无视上面这个访问控制,访问控制是对外部调用来说的。
  • private: public:成员访问限定符顺序、数量无限制。
  • 最佳做法:所有数据成员都设为private,部分函数设为public

成员函数

  • 普通函数怎么定义就怎么定义。
  • 类作为一个整体,不用定义先后问题。也就是说,一个东西后被定义,先定义的成员函数也是可以用的。
  • 可以选择跑到类声明外去定义成员函数。在外面要加作用域限定符来说明这是谁的:
1
2
3
4
5
6
7
class student{
xxx
};
void student::display()
{
xxx
}
  • 通过对象调用成员函数的时候,其实是通过一个隐含的this指针来指向要操作的对象。

C++下struct和class用起来的区别

如果不加成员访问限定符,会默认给成员函数和数据成员一个属性。在struct这里默认是publicclass这里默认是private,这就是唯一的区别。

作用域问题

  • 类内的数据成员可以和全局变量、和成员函数内自动变量重名,在会引起混淆的情况下
    • 访问全局变量:::a
    • 访问类内数据成员:类名::a
    • 访问自动变量: a

称呼转变

  • 对应结构体类型,对象对应变量。
  • 可被外界调用的公有函数,类的公共/对外接口
  • 对象.公有函数(形参),这个函数叫方法,调用过程叫消息传递

构造函数

结构体方式对象初始化

如果全部的数据成员都是公有的(只要有人是私有就不行),那可以用结构体一样的方式进行初始化。

1
2
3
4
5
6
Time a = {1,2,3};
Time a{1,2,3};
Time a(1,2,3);//C++20开始
//上面都是可以的
Time a = (1,2,3)
//这句不对的,后面是逗号表达式,相当于Time a = 3;

声明时统一初始化

C++11后可以

1
2
3
4
5
class Time{
public:
int h = 12;
int m = 0;
};

构造函数初始化

1
2
3
4
5
6
7
class Time{
private:
int h;
int m;
public:
Time(int hi,int mi);
};
  • 构造函数没有类型,没有void,也不是缺省int。特殊语法。
  • 构造函数可以带参数。
  • 构造函数必须放在公有这里
  • 你不自己去写,默认会有一个构造函数在那里的,只是内容是空的,叫无参空体。如果你写了,默认这个构造函数就没了。如果你的构造函数是有参数的,你再按照普通的不初始化的方式去定义一个对象,是不行的。你要使用刚才结构体方式那3种方式给这个构造函数传参,只不过之前这3种形式的内容是用来给公有数据成员赋初值,现在是给你的构造函数传参数。
  • 一种特殊写法:参数初始化表,形参表以后,加个冒号,数据成员(形参),一个个搞下来。这东西只能在定义处有,声明处不能写。
1
2
3
4
5
6
7
8
9
class Time{
private:
int h;
int m;
public:
Time(int hi, int mi) : h(hi), m(mi)
{
}
};
  • 构造函数要灵活应用重载函数和参数默认值来实现更好的效果。
  • 构造函数的使用可以是上面说的那3种形式,这叫隐式使用,也可以显式使用:Time a = Time(1,2);,就是一个正常函数,给你返回一个被初始化后的对象。

析构函数

一些对象的使用过程中可能会申请内存什么的,这时需要析构函数来把这些内存释放掉。

  • 写法是~Time(),一般来讲,~是不能出现在标识符里的,但是这里是一个特殊语法。
  • 无参数,不能重载,不能显式调用,必须公有。

对象数组初始化

1
Time a[] = {{1, 2}, Time(3,4), Time{5, 6}};

都可以的,没定义构造函数的时候,这是数据成员一个个赋值;定义了构造函数的时候,这是参数一个个传

this

  • 上面这里提到了this指针。
  • 所以说,你是不能在一个成员函数里面搞一个指向这个类的对象的,名字叫this的指针的,这会报错。换个名字。
  • 然后你可以在成员函数里面直接使用这个this指针。虽然没有明写,但它存在,且可以用。
1
2
3
4
void Time::set(int hi, int mi){
this->h = hi;
this->m = mi;
}

赋值

对象都可以用=来赋值,逻辑是直接把内存空间复制过去
神剑把定义新对象的这个赋值叫作复制,根本没区别的东西,强行进行了区分,区分就区分吧

友元

  • 支持普通的函数、其他类的成员函数、整个其他类
  • 在要被访问的类的声明里面随便找个地方(完全不管public private),把那个被授权的东西的声明写法前面加一个friend
1
2
3
4
5
6
7
8
class Time{
private:
int h;
int m;
friend void count(int a, int b);
friend void Date::show();
friend class TD;
};

现在,void count(int a, int b)就可以访问Time类的h和m了。但由于count不是Time的成员函数,没有this指针,访问这些私有数据成员要有一个具体的对象才行。

  • 友元的授权是单向的。
  • 友元不能传递。
  • C++规定同类的不同对象互为友元。Time类的a对象可以去访问Time类的b对象的私有数据成员。