C++结构体的赋形之记

C++结构体详解

  • 前言
    • 一、结构体的基本概念与意义
    • 二、结构体的定义与声明
    • 三、结构体变量的创建与初始化
      • 1.声明后逐个赋值
      • 2.聚合初始化
      • 3.使用构造函数初始化
    • 四、结构体成员的访问
    • 五、C++结构体与C结构体的差异
    • 六、结构体与类的比较
      • 1.相似之处
      • 2.关键区别
    • 七、结构体的高级用法
      • 1.嵌套结构体
      • 2.结构体数组
      • 3.结构体指针
      • 4.结构体的类型别名
        • 4.(1).使用`typedef`
        • 4.(2).使用`using`
      • 5.结构体中的成员函数
    • 八、结构体作为函数参数与返回值
      • 1.结构体作为函数参数
        • 1.(1)值传递
        • 1.(2).引用传递
        • 1.(3).指针传递
      • 2.结构体作为函数返回值
  • 总结

前言

在C++中,结构体(struct)是一种非常重要的用户自定义数据类型,它能够将多个不同类型的数据元素组合在一起,形成一个有机的整体。这种数据组织方式不仅提高了代码的可读性和可维护性,还为处理复杂数据提供了便捷的手段。

一、结构体的基本概念与意义

在程序设计中,我们经常需要处理由多个相关数据组成的信息单元。例如,描述一个学生需要学号、姓名、成绩等数据;描述一个点需要x坐标和y坐标;描述一个汽车需要品牌、型号、年份等信息。这些数据虽然类型不同,但彼此关联,共同构成了一个完整的实体信息。

如果将这些相关数据分散存储,不仅会增加代码的复杂性,还会降低数据的关联性。结构体的出现正是为了解决这一问题------它允许我们将多个不同类型的变量封装在一个统一的类型中,形成一个新的复合数据类型。这种数据类型能够直观地反映数据之间的内在联系,使代码结构更加清晰,数据处理更加高效。

结构体在C++中并非全新的概念,它源自C语言,但在C++中得到了扩展和增强,具备了更多面向对象的特性,这使得结构体在C++中具有更广泛的应用场景。

二、结构体的定义与声明

要使用结构体,首先需要定义结构体类型。结构体的定义使用struct关键字,其基本语法结构如下:

cpp 复制代码
struct 结构体名称 {
    数据类型1 成员变量1;
    数据类型2 成员变量2;
    // ... 更多成员变量
};

其中,结构体名称是我们为这个新数据类型指定的名字,通常采用首字母大写的驼峰命名法,以区分普通变量。花括号内的部分是结构体的成员变量,它们可以是任意基本数据类型(如int、float、string等),也可以是其他自定义类型。

以学生信息为例,我们可以定义一个Student结构体:

cpp 复制代码
struct Student {
    int id;
    std::string name;
    float grade;
};

这个结构体包含三个成员变量:id(整型,存储学号)、name(字符串型,存储姓名)、grade(浮点型,存储成绩)。

通过这个定义,我们创建了一个名为Student的新类型,它可以用来表示一个学生的完整信息。

注意:结构体定义的末尾必须加上分号,这是C++语法的要求,用于标记结构体定义的结束。

三、结构体变量的创建与初始化

定义结构体类型后,我们就可以像使用基本数据类型一样,创建该类型的变量(也称为结构体实例)。结构体变量的创建和初始化有多种方式,每种方式都有其适用场景。

1.声明后逐个赋值

最直观的方式是先声明结构体变量,然后通过成员访问运算符(.)为每个成员变量逐个赋值。例如:

cpp 复制代码
Student student1;
Student student2;

// 给student1成员变量赋值
student1.id = 241206211;
student1.name = "why";
student1.grade = 89.5f;

在这个例子中,我们首先声明了两个Student类型的变量student1student2,然后为student1的每个成员变量分别赋值。

这种方式的优点是灵活性高,可以根据需要随时修改成员变量的值;

缺点是当结构体成员较多时,赋值操作会显得繁琐。

2.聚合初始化

对于结构简单的结构体,我们可以在声明变量时直接进行初始化,这种方式称为聚合初始化。聚合初始化通过一个初始化列表,按照成员变量在结构体中的声明顺序为其赋值,语法如下:

cpp 复制代码
Student student3 = {241206228, "hy", 90.0f};

这里,初始化列表中的第一个值241206228赋值给第一个成员变量id,第二个值"hy"赋值给name,第三个值90.0f赋值给grade

聚合初始化的优点是简洁高效,适合在变量声明时一次性完成初始化,
要求初始化列表中的值的类型和顺序必须与结构体成员变量的类型和顺序一致

3.使用构造函数初始化

在C++中,结构体与类一样可以包含构造函数。构造函数是一种特殊的成员函数,它的名称与结构体名称相同,没有返回值,用于在创建结构体对象时初始化其成员变量。使用构造函数初始化结构体变量是一种更加灵活和安全的方式。

例如,我们可以为Student结构体定义一个带参数的构造函数:

cpp 复制代码
struct Student {
    int id;
    std::string name;
    float grade;

    Student(int studentId, std::string studentName, float studentGrade)
        : id(studentId), name(studentName), grade(studentGrade) {}
};

这个构造函数接受三个参数,分别对应idnamegrade,并通过初始化列表的方式为成员变量赋值。初始化列表位于构造函数参数列表之后,使用冒号(:)开头,各成员的初始化表达式之间用逗号分隔。

除了初始化列表,我们也可以在构造函数体内为成员变量赋值:

cpp 复制代码
Student(int studentId, std::string studentName, float studentGrade) {
    id = studentId;
    name = studentName;
    grade = studentGrade;
}

这两种方式的效果类似,但初始化列表的效率更高,因为它直接初始化成员变量,而构造函数体内的赋值是先初始化成员变量再进行赋值操作。

对于一些没有默认构造函数的成员变量(如某些自定义类型),必须使用初始化列表进行初始化。

使用构造函数创建结构体变量的方式如下:

cpp 复制代码
Student student1(10, "John", 9.5);

这里,我们通过Student结构体的构造函数创建了student1对象,并传入三个参数分别初始化idnamegrade

注意:一旦我们为结构体定义了任何构造函数(无论是有参还是无参),编译器就不会再自动生成默认的无参构造函数。如果我们需要使用无参构造函数创建对象,就必须显式地定义它

例如:

cpp 复制代码
struct Student {
    int id;
    std::string name;
    float grade;

    // 无参构造函数
    Student() {}

    // 有参构造函数
    Student(int studentId, std::string studentName, float studentGrade)
        : id(studentId), name(studentName), grade(studentGrade) {}
};

这样,我们就可以同时使用两种方式创建Student对象:

cpp 复制代码
Student student4; // 使用无参构造函数
Student student5(11, "Alice", 85.5); // 使用有参构造函数

四、结构体成员的访问

创建结构体变量后,我们需要访问其成员变量来获取或修改数据。结构体成员的访问通过成员访问运算符(.)实现,其语法为:

cpp 复制代码
结构体变量名.成员变量名

例如,访问student1的各个成员并输出:

cpp 复制代码
cout << "学生ID:" << student1.id << endl;
cout << "学生姓名:" << student1.name << endl;
cout << "学生成绩:" << student1.grade << endl;

**结构体指针:**访问成员变量时需要使用箭头运算符(->),其语法为:

cpp 复制代码
结构体指针->成员变量名

这等价于先对指针进行解引用,再使用.运算符访问成员,(*结构体指针).成员变量名。例如:

cpp 复制代码
Student* studentPtr = &student1;
cout << "学生ID:" << studentPtr->id << endl; // 等价于(*studentPtr).id

成员访问运算符的优先级较高,在复杂表达式中可以清晰地指定访问的成员,确保代码的正确性。

五、C++结构体与C结构体的差异

虽然结构体在C和C++中都存在,但两者之间存在一些重要差异,这些差异体现了C++对结构体的扩展和增强。

最显著的差异是C++的结构体可以包含成员函数 ,而C的结构体只能包含成员变量

在C++中,结构体不仅是数据的集合,还可以包含操作这些数据的函数,这使得结构体具备了面向对象的特性。

例如,我们在结构体中定义一个用于打印自身信息的成员函数:

cpp 复制代码
struct Book {
    std::string title;
    std::string author;
    int pages;

    // 成员函数:打印书籍信息
    void printInfo() const {
        std::cout << "书名: " << title 
                  << ", 作者: " << author 
                  << ", 页数: " << pages << std::endl;
    }
};

在这个例子中,printInfoBook结构体的成员函数,它可以直接访问结构体的成员变量titleauthorpages,用于打印书籍的完整信息。

另一个重要差异是C++的结构体支持构造函数和析构函数,而C的结构体没有这些概念。

如上面所述,构造函数用于初始化结构体对象,析构函数则用于在对象销毁时释放资源,这进一步增强了C++结构体的功能。(析构函数再类中会进行详细的讲解)

此外,C++的结构体支持访问控制(public、private、protected),虽然其默认访问权限为public(与类的默认private不同),但这也令结构体可以实现封装特性,控制成员的访问权限。

而C的结构体成员默认都是公开的,无法进行访问控制。

六、结构体与类的比较

在C++中,结构体(struct)和类(class)有很多相似之处,但也存在一些关键区别,理解这些区别有助于我们在实际编程中选择合适的类型。

1.相似之处

结构体和类在很多方面是一致的:

  • 都可以包含成员变量和成员函数,用于存储数据和实现操作。
  • 都支持访问控制修饰符(public、private、protected),可以控制成员的访问权限。
  • 都支持继承和多态,能够实现代码的复用和扩展。
  • 都可以拥有构造函数、析构函数、拷贝构造函数等特殊成员函数。

2.关键区别

结构体和类的主要区别体现在默认访问权限和使用习惯上:

2.(1). 默认访问权限

  • 结构体的默认成员访问权限为public,即如果没有显式指定访问修饰符,结构体的成员可以被外部直接访问。
  • 类的默认成员访问权限为private,即如果没有显式指定访问修饰符,类的成员只能在类内部被访问,外部无法直接访问。

例如:

cpp 复制代码
struct StructExample {
    int x; // 默认public,外部可直接访问
};

class ClassExample {
    int y; // 默认private,外部不可直接访问
};

2.(2). 使用习惯

  • 结构体通常用于存储纯数据,强调数据的聚合,成员变量一般是公开的,方便外部直接访问和修改。一般用于表示坐标、日期、学生信息等简单数据结构。
  • 类则更多地用于实现面向对象的封装,将数据和操作数据的函数结合在一起,通过私有成员保护数据,只提供公开的接口供外部使用。一般用于实现复杂的业务逻辑、数据处理等功能。

例如,一个表示点的结构体和一个表示矩形的类:

cpp 复制代码
struct Point {
    int x;
    int y; // 公开成员,方便直接操作坐标
};

class Rectangle {
private:
    int width;
    int height; // 私有成员,保护数据

public:
    // 公开,用于操作数据
    void setDimensions(int w, int h) {
        width = w;
        height = h;
    }
    int area() const {
        return width * height;
    }
};

七、结构体的高级用法

随着对结构体理解的深入,我们可以利用其高级特性处理更复杂的问题。以下是结构体的几种高级用法:

1.嵌套结构体

结构体可以包含其他结构体作为成员,这种结构称为嵌套结构体。嵌套结构体能够更好地反映数据之间的层次关系,使数据组织更加清晰。

例如,我们可以定义一个Address结构体表示地址信息,然后在Person结构体中嵌套Address作为成员,以表示一个人的住址:

cpp 复制代码
struct Address {
    std::string city;
    std::string street;
    int houseNumber;
};

struct Person {
    std::string name;
    int age;
    Address address; // 嵌套结构体,存储地址信息
};

在使用嵌套结构体时,访问内层结构体的成员需要通过外层结构体的成员变量,使用多个.运算符:

cpp 复制代码
Person person;
person.name = "Why";
person.age = 21;
person.address.city = "BX"; // 访问嵌套结构体的成员
person.address.street = "5th Avenue";
person.address.houseNumber = 3301;

std::cout << person.name << " lives at "
          << person.address.houseNumber << " "
          << person.address.street << ", "
          << person.address.city << std::endl;

这段代码创建了一个Person对象person,为其姓名、年龄以及地址信息(城市、街道、门牌号)赋值,并输出了完整的居住信息。嵌套结构体使得个人信息和地址信息的关联更加直观,代码的可读性也得到了提高。

2.结构体数组

当我们需要处理多个同类型的结构体对象时,可以使用结构体数组。结构体数组是由结构体元素组成的数组,它能够方便地存储和管理批量数据。

例如,我们可以创建一个Student类型的数组,存储多个学生的信息:

cpp 复制代码
struct Student {
    int id;
    std::string name;
    float grade;
};

// 创建包含3个学生信息的数组
Student students[3] = {
    {1001, "Alice", 89.5f},
    {1002, "Bob", 92.0f},
    {1003, "Charlie", 85.0f}
};

这里,students是一个包含3个Student元素的数组,每个元素通过聚合初始化的方式设置了学号、姓名和成绩。

访问结构体数组中的元素及其成员时,需要先通过数组下标获取元素,再使用.运算符访问成员:

cpp 复制代码
for (int i = 0; i < 3; i++) {
    std::cout << "学生ID: " << students[i].id
              << ", 姓名: " << students[i].name
              << ", 成绩: " << students[i].grade << std::endl;
}

通过循环遍历结构体数组,我们可以依次输出每个学生的信息,这种方式在处理批量数据时非常高效。

3.结构体指针

指针是C++中一种强大的工具,结构体指针则是指向结构体对象的指针。使用结构体指针可以更灵活地操作结构体对象,尤其是在函数参数传递和动态内存分配中。

定义结构体指针的方式与定义其他类型指针类似:

cpp 复制代码
struct Car {
    std::string brand; // 品牌
    std::string model; // 型号
    int year; // 年份
};

Car car = {"BMW", "M5", 2019};
Car* carPtr = &car; // 结构体指针指向car对象

通过结构体指针访问成员变量时,需要使用箭头运算符(->):

cpp 复制代码
std::cout << "品牌: " << carPtr->brand << std::endl;
std::cout << "型号: " << carPtr->model << std::endl;
std::cout << "年份: " << carPtr->year << std::endl;

carPtr->brand等价于(*carPtr).brand,箭头运算符是一种简化的语法,使代码更加简洁易读。

结构体指针在函数中传递时,可以避免结构体对象的拷贝,提高程序的效率,尤其是当结构体包含大量成员变量时。

4.结构体的类型别名

为了简化结构体类型的使用,尤其是当结构体名称较长或使用频繁时,我们可以为结构体类型定义别名。C++提供了两种方式定义类型别名:typedefusing(C++11及以上标准)。

4.(1).使用typedef

typedef关键字可以为已有的类型创建一个新的名称(别名),其语法如下:

cpp 复制代码
typedef 原类型 别名;

例如,为Car结构体定义别名WXY

cpp 复制代码
typedef Car WXY;

之后,我们就可以使用WXY作为Car的别名来定义变量:

cpp 复制代码
WXY car1 = {"Toyota", "Camry", 2020};
WXY* carPtr1 = &car1;

我们也可以在定义结构体的同时使用typedef

cpp 复制代码
typedef struct {
    int id;
    std::string name;
    float grade;
} StudentType;

这里,StudentType是一个匿名结构体的别名,我们可以直接使用StudentType定义变量:

cpp 复制代码
StudentType studentType = {1004, "Daisy", 95.0f};
4.(2).使用using

C++11引入了using关键字来定义类型别名,其语法更加直观:

cpp 复制代码
using 别名 = 原类型;

例如,为一个结构体定义别名StudnetType2

cpp 复制代码
using StudnetType2 = struct {
    int id;
    std::string name;
    float grade;
};

使用using定义的别名与typedef具有相同的效果,但using的语法更清晰,尤其是在定义复杂类型(如模板类型)的别名时优势更明显。

类型别名并不会创建新的类型,它只是原类型的一个替代名称,这意味着通过别名定义的变量与原类型定义的变量是兼容的。

5.结构体中的成员函数

前文已经提到,C++的结构体可以包含成员函数,这些函数可以直接访问结构体的成员变量,用于实现与结构体相关的操作。结构体的成员函数使结构体不仅仅是数据的容器,还具备了处理数据的能力,这是C++结构体面向对象特性的体现。

例如,Book结构体中的printInfo函数就是一个成员函数,它用于打印书籍的信息:

cpp 复制代码
struct Book {
    std::string title;
    std::string author;
    int pages;

    // 成员函数:打印书籍信息
    void printInfo() const {
        std::cout << "书名: " << title 
                  << ", 作者: " << author 
                  << ", 页数: " << pages << std::endl;
    }
};

成员函数的定义可以在结构体内部(如上述例子),也可以在结构体外部。在结构体外部定义成员函数时,需要使用作用域运算符(::)指定函数所属的结构体:

cpp 复制代码
struct Book {
    std::string title;
    std::string author;
    int pages;

    // 声明成员函数
    void printInfo() const;
};

// 在结构体外部定义成员函数
void Book::printInfo() const {
    std::cout << "书名: " << title 
              << ", 作者: " << author 
              << ", 页数: " << pages << std::endl;
}

成员函数的调用方式与访问成员变量类似,通过结构体对象或指针使用.->运算符:

cpp 复制代码
Book book1 = {"Bob", "Charlie", 2019};
book1.printInfo(); // 通过对象调用成员函数

Book* bookPtr = &book1;
bookPtr->printInfo(); // 通过指针调用成员函数

成员函数可以像普通函数一样接受参数、返回值,也可以被const修饰(如printInfo() const)。被const修饰的成员函数承诺不会修改结构体的成员变量,这有助于提高代码的安全性和可读性。

八、结构体作为函数参数与返回值

结构体作为一种数据类型,可以像基本数据类型一样作为函数的参数和返回值,这使得函数能够方便地处理复杂数据。

1.结构体作为函数参数

结构体作为函数参数时,有三种传递方式:值传递、引用传递和指针传递。

1.(1)值传递

值传递是将结构体对象的副本传递给函数参数,函数内部对参数的修改不会影响原对象。例如:

cpp 复制代码
struct Point {
    int x;
    int y;
};

// 通过值传递结构体
void printPoint(Point p) {
    std::cout << "Point(" << p.x << ", " << p.y << ")" << std::endl;
}

调用该函数时,会创建p1的一个副本传递给printPoint

cpp 复制代码
Point p1 = {10, 20};
printPoint(p1); // 传递p1的副本

值传递的优点是简单直观,不会影响原对象,但缺点是当结构体较大时,复制操作会消耗较多的内存和时间,降低程序效率。

1.(2).引用传递

引用传递是将结构体对象的引用传递给函数参数,函数内部对参数的修改会直接影响原对象。引用传递通过&符号声明:

cpp 复制代码
// 通过引用传递结构体
void movePoint(Point& p, int dx, int dy) {
    p.x += dx;
    p.y += dy;
}

调用该函数时,pp1的引用,对p的修改会反映到p1上:

cpp 复制代码
movePoint(p1, 5, -5); // p1的x增加5,y减少5
printPoint(p1); // 输出修改后的p1

引用传递不会产生对象的副本,效率较高,同时可以方便地修改原对象,是结构体参数传递的常用方式。

1.(3).指针传递

指针传递是将结构体对象的地址(指针)传递给函数参数,函数内部通过指针访问和修改原对象。指针传递通过*符号声明参数类型:

cpp 复制代码
// 通过指针传递结构体
void movePoint2(Point* p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

调用该函数时,需要传递结构体对象的地址:

cpp 复制代码
movePoint2(&p1, 5, -5); // 传递p1的地址,p1的x再增加5,y再减少5
printPoint(p1); // 输出再次修改后的p1

指针传递与引用传递类似,都不会产生对象的副本,效率较高,并且可以修改原对象。两者的主要区别在于指针可以为空,而引用必须指向一个有效的对象。

2.结构体作为函数返回值

函数可以返回结构体类型的值,这使得函数能够将处理后的复杂数据返回给调用者。例如:

cpp 复制代码
// 返回结构体的函数
Point movePoint3(Point* p, int dx, int dy) {
    Point p2 = *p; // 复制原对象
    p2.x += dx;
    p2.y += dy;
    return p2; // 返回修改后的副本
}

这个函数接受一个结构体指针和两个偏移量,创建原对象的副本并修改,然后返回修改后的副本。调用该函数时,可以将返回值赋值给一个结构体变量:

cpp 复制代码
auto p2 = movePoint3(&p1, 5, -5); // 传进p1,p2是修改后的副本
printPoint(p1); // 原对象p1未被修改
printPoint(p2); // 输出p2

那么函数也可以返回修改后的原对象:

cpp 复制代码
Point movePoint4(Point* p, int dx, int dy) {
    (*p).x += dx; // 通过解引用修改原对象
    (*p).y += dy;
    return *p; // 返回修改后的原对象
}

调用该函数时,原对象会被修改,同时返回修改后的原对象:

cpp 复制代码
auto p3 = movePoint4(&p1, 5, -5); // p1被修改,p3是p1的副本
printPoint(p1); // 输出修改后的p1
printPoint(p3); // 输出p3(与p1相同)

返回结构体时,函数会创建返回值的副本,因此如果结构体较大,可能会影响效率。在这种情况下,可以考虑返回结构体指针或引用,但需要注意内存管理,避免返回局部变量的指针或引用(它们在函数结束后会被销毁)。

总结

结构体是C++中一种灵活而强大的数据类型,它允许我们将多个相关数据组合成一个整体,提高了代码的组织性和可读性。

与C语言的结构体相比,C++的结构体增加了成员函数、构造函数等面向对象的特性,使其更接近类,但在默认访问权限和使用习惯上与类有所区别。在实际编程中,我们可以根据具体需求选择使用结构体或类:结构体适合存储简单的聚合数据,类适合实现复杂的封装和业务逻辑。

希望大家通过本文的了解了结构体的定义、初始化、成员访问、高级用法以及与函数的结合使用。当然如果有什么地方和细节上的不足,欢迎大家在评论区进行指正和批评,这节内容就先到这里了。

相关推荐
稚肩1 小时前
如何在linux中使用Makefile构建一个C++工程?
linux·运维·c++
啊森要自信1 小时前
【QT】常⽤控件详解(七)容器类控件 GroupBox && TabWidget && 布局管理器 && Spacer
linux·开发语言·c++·qt·adb
源代码•宸2 小时前
C++高频知识点(二十)
开发语言·c++·经验分享·epoll·拆包封包·名称修饰
重启的码农2 小时前
ZeroTier 源码解析 (2) 节点 (Node)
c++·网络协议
重启的码农2 小时前
ZeroTier源码解析 (3) 身份 (Identity)
c++·网络协议
郝学胜-神的一滴3 小时前
Horse3D引擎研发笔记(一):从使用Qt的OpenGL库绘制三角形开始
c++·qt·3d·unity·图形渲染·unreal engine
草莓熊Lotso3 小时前
《解锁 C++ 起源与核心:命名空间用法 + 版本演进全知道》
c++·经验分享·笔记·其他
玖剹3 小时前
深入解析Linux信号处理机制
linux·运维·服务器·网络·c++·ubuntu
愿天堂没有C++4 小时前
剑指offer第2版——面试题2:实现单例
c++·设计模式·面试