15. 结构体
结构体是属于用户自定义的数据类型,允许存储不同数据。
语法:struct 结构体名 {结构体成员};
通过结构体创建变量的三种方式:
- struct 结构体名 变量名
- struct 结构体名 变量名={成员1值,成员2值...}
- 定义结构体时顺便创建变量。
struct Student{ string name; int age; int score; }s3; int main() { // 法1 struct Student s1; s1.name = "李四"; s1.age = 50; s1.score = 100; cout<<s1.name<<" "<<s1.age<<" "<<s1.score<<endl; // 法2 struct Student s2={"张三",18,100}; cout<<s2.name<<" "<<s2.age<<" "<<s2.score<<endl; // 法3 在上面末尾定义 s3.name = "王五"; s3.age = 10; s3.score = 20; cout<<s3.name<<" "<<s3.age<<" "<<s3.score<<endl; }
结构体数组
上面我们只是定义了一个一个的学生,实际上多个学生的信息我们是要放到一起的,
语法:struct 结构体名 数组名[个数] = {{}, {}, {}...}
struct Student{ string name; int age; int score; }; int main() { struct Student arr[5]={ {"张三",18,100}, {"李四",50,50}, {"王五",12,30} }; //范围内访问超出元素个数的如arr[3] 字符串不显示 数字为0 arr[1].name = "李十四"; cout<<arr[1].name<<"的分数为:"<<arr[1].score<<endl; }
结构体指针
通过操作符 -> 可以通过结构体指针访问结构体属性,后面学的类对象也是。
记得声明结构体指针时是 (struct) 结构体名 *p=&元素/数组名 即声明xx结构体类型的指针变量。
int main() { struct Student s={"张三",18,100}; // Student s={"张三",18,100}; struct Student *p = &s; // Student *p = &s; cout<<p->name<<"的分数为:"<<p->score<<endl; // 张三的分数为:100 }
如果你写的是p.name 它会自动帮你转换成p->name,明白指针的话(*p).score也行。
结构体嵌套
struct Student{ string name; int age; int score; }; struct Teacher{ int id; string name; int age; struct Student stu; }; int main() { struct Student s1={"张三",18,95}; struct Teacher t = {001,"李老师",80,s1}; t.stu.name = "张三万"; cout<<t.name<<"的学生"<<t.stu.name<<"的分数为:"<<t.stu.score<<endl; // 李老师的学生张三万的分数为:95 }
struct Student{ string name; int age; int score; }; struct Teacher{ int id; string name; int age; struct Student stu; }; int main() { struct Student s1={"张三",18,95}; struct Teacher t = {001,"李老师",80,s1}; t.stu.name = "张三万"; cout<<t.name<<"的学生"<<t.stu.name<<"的分数为:"<<t.stu.score<<endl; // 李老师的学生张三万的分数为:95 }
当然也可以通过for循环给t.arr[i]赋值或者 t.arr[i].name等的赋值。
struct Student{ string name; int age; int score; }; struct Class{ int id; struct Student arr[5]; }; struct Teacher{ int id; string name; int age; struct Class c; }; int main() { struct Student s1={"张三",18,95}; struct Student s2={"张四",19,97}; struct Student s3={"张五",20,100}; struct Class c1={001,s1,s2,s3}; struct Teacher t = {001,"李老师",80,c1}; for(int i=0;i<3;i++){ cout<<t.name<<"的"<<t.c.id<<"班"<<"学生"<<t.c.arr[i].name<<"的分数为:"<<t.c.arr[i].score<<endl; } /* 李老师的1班学生张三的分数为:95 李老师的1班学生张四的分数为:97 李老师的1班学生张五的分数为:100 */ }
一般用结构体指针接收结构体数组:
struct Student{ string name; int age; int score; }; struct Class{ int id; struct Student *stu; }; int main() { struct Student s[3]={ {"张五",20,100}, {"张四",19,97}, {"张五",20,100} }; struct Class c1={001,s}; for(int i=0;i<3;i++){ cout<<c1.id<<"班的"<<"学生"<<c1.stu[i].name<<"的分数为:"<<c1.stu[i].score<<endl; } /* 1班的学生张五的分数为:100 1班的学生张四的分数为:97 1班的学生张五的分数为:100 */ }
这样只开辟一个指针变量的大小,用别人家地址存放的东西,节省空间不是吗。
结构体做函数参数
值传递
struct Student{ string name; int age; int score; }; void details(Student stu){ // struct Student stu cout<< stu.name<<","<<stu.age<<","<<stu.score<<endl; stu.name="xxxxxx"; } // 值传递 int main() { struct Student s1={"张五",20,100}; details(s1); // 张五,20,100 }
地址传递 节省空间
void details(Student *stu){ // 这个时候只能用箭头 cout<< stu->name<<","<<stu->age<<","<<stu->score<<endl; // cout << (*stu).name << "," << (*stu).age << "," << (*stu).score << endl; 这样也行 stu->name="xxxxxx"; } // 地址传递 int main() { struct Student s1={"张五",20,100}; details(&s1); cout<< s1.name<<","<<s1.age<<","<<s1.score<<endl; // xxxxxx,20,100 }
地址传递数组(结构体数组也是数组,只是内部元素不同)
void details(Student *stu) { // 指针指向这个数组第一个 for (int i = 0; i < 3; i++) { // 这个时候不能用箭头 cout << stu[i].name << "," << stu[i].age << "," << stu[i].score << endl; stu[i].name = "xxxxxx"; } } int main() { struct Student s[3] = { {"张五", 20, 100}, {"张四", 19, 97}, {"张五", 20, 100} }; details(s); // 张五,20,100 再强调一下虽然s和&s cout出来的值一样,这里也不能写&s for(int i = 0; i < 3; i++) { cout << s[i].name << "," << s[i].age << "," << s[i].score << endl; } } /* 张五,20,100 张四,19,97 张五,20,100 xxxxxx,20,100 xxxxxx,19,97 xxxxxx,20,100 */
结构体作为返回值
struct Student { int id; string name; }; Student foo() { Student s = { 99,"xx" }; return s; } int main() { Student p = foo(); cout << p.name << endl; }
愿意指针引用也行,但没必要,不做其他声明也只能用一次显然不好。后面讲类对象的时候会练,这里先略。
结构体中const应用场景
将函数中的形参改为指针,可以减少内存空间,而且不会复制新的副本出来。(指针只占8个字节,而如果是重新形参Student stu接收,要开辟该结构体一样大小的空间。)此时也就是所谓的地址传递,形参改变会改变实参。
为了防止误操作,可以加个const。即我们前面讲的const修饰指针(指向的值不可以变)。
这一看到这里飘红了。
当然这样也不对
当然这样就行了
要明白在数组那讲的const修饰指针修饰的是什么地方。前两个是地址对应的值,后面那个是地址。
二、C++进阶
1. 内存分区模型
C++程序在执行时,将内存大方向划分为4个区域:
代码区:存放函数体的二进制代码,又操作系统进行管理的(程序运行前)
全局区:存放全局变量和静态变量以及常量(程序运行前)
栈区:由编译器自动分配释放,存放函数的参数值,局部变量等(程序运行后)
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。(程序运行后)
内存四区的意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。
程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域:
代码区:
存放cpu执行的机器指令
代码区是 共享 的,目的是对于频繁被执行的程序,只需在内存中有一份代码即可。
代码区是 只读 的,使其只读的原因是防止程序意外地修改了它的指令。
全局区:
全局变量和静态变量存放在此。
全局区还包含了常量区,字符串常量和其他常量也存放在此。
该区域地数据在程序结束后由操作系统释放。
通过打印地址可以发现,局部变量和局部常量地址相近;
全局变量、全局常量静态变量字符串常量地地址相近。
程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值,局部变量等。
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
注意返回地址声明时要声明为指针类型。
int* func() { int a = 10; return &a; } int main() { int* p = func(); cout << *p << endl; //10 cout << *p << endl; //2025425288 }
我们上面知道,编译器自动分配释放栈区数据,我们最后返回a的地址,此时func函数结束,里面的数据被释放。我们下面再通过*p去取的时候,该块内存以及没有访问权限或者访问出乱码了。
之所以第一次嫩更成功,是因为此时编译器给我们做了一次保留,担心是我们误操作。但第二次就不保留了。(可以static int a = 10; )
堆区:
- 有程序员分配释放,若程序员不释放,程序结束之后有操作系统回收
- 在C++中主要利用new在堆区中开辟内存
注意返回地址声明时要声明为指针类型。
int* func() { //new返回该数据类型的指针,所以用指针接收 new的是float就用 float *接收 int *a = new int(10); return a; // a存的是堆区数据的地址,他返回给主函数的*p去接收。 } int main() { int* p = func(); // 这里的int *p=a 相当于以前的 int a=10; int *p=&a; cout << *p << endl; // 10 cout << *p << endl; // 10 delete p; cout << *p << endl; // 引发异常 }
int* func() { int *arr = new int[10]; //数组中括号 arr[0] = 99; return arr; // arr是数组首地址,他返回给主函数的*p去接收。 } int main() { int* p = func(); cout << p[0] << endl; // 99 cout << p[0] << endl; // 99 delete[] p; // 释放数组全部空间 }
这样又多了种定义变量和数组的方式 int *arr = new int[10];其中arr未数组名,也是它的地址。
这里new和delete的知识后面讲析构函数还会用到。
2. 引用
int main() { int a = 10; int& b = a; // int * const b = &a; cout << b << endl; // 10 b = 99; // *b = 99; cout <<a<<" "<< b << endl; // 99 99 }
这个怎么理解呢,以前存放10的这块地址叫a,以后也叫b了。即此时a,b都是变量,10的名字。
注意,如果写成了int b = &a;会报错(你怎么能让一个16进制的数字去赋值给一个int类型的变量呢?),别和前面学的指针 int * b = &a;搞混。
int main() { int a = 10; int *b = &a; cout << b << endl; // 00AFF804 *b = 99; cout <<a<<" "<< *b << endl; // 99 99 }
- (开始定义时) 引用必须要初始化。(define、const也是 但const int &a去做函数参数可以不)
- 引用之后就不可以改变。
int main() { int a = 10,c=100; int &b = a; b = c; // 相当于执行 a=100或者说是b=100 cout << b << endl; // 100 这是进行赋值,而不是更改引用。 b = 99; cout <<a<<" "<< b << endl; // 99 99 }
可以用引用代替指针接收
void swap1(int& a, int& b) { int tem = a; a = b; b = tem; } void swap2(int* a, int* b) { int tem = *a; *a = *b; *b = tem; } int main() { int a = 3; int b = 5; swap1(a, b); cout <<a<<" "<< b << endl; // 5 3 swap2(&a, &b); cout << a << " " << b << endl; // 3 5 }
引用做函数返回值
- 不要返回局部变量的引用
- 函数可以是左值
int& foo1() { int a = 10; // 局部变量 栈区 这个函数结束后释放 return a; } int& foo2() { static int a = 99; // 静态变量 全局区 程序结束后释放 return a; } int main() { int& a = foo1(); cout << a << endl; // 10 编译器做了保留 cout << a << endl; // 2038794632 内存已经被释放 int& b = foo2(); cout << b << endl; // 99 cout << b << endl; // 99 foo2() = 10000; // 其实就是赋值 a=10000; cout << b << endl; // 10000 }
注意
int& foo2() { static int a = 99; // 静态变量 全局区 程序结束后释放 return a; }
像这样用&声明名的函数,返回的内容还是a,foo2()结果依然是99,但此时你不能用int &类型的变量去接收,即int &b = foo2()是错的,即int &b = 99是错的,但const int &b = foo2()可以。 所以如果我像直接int &b = foo2()那就在函数声明时加上&,要么就 int b=foo2()一个最简单的接收函数返回值。
const修饰防止误操作
// int a = 99; // int &b = a; //可以 // int &b = 5; 不能这样写 const int& b = 10; // 可以 不可修改 // const int *p = 5; int* const p = 10; 不可以
void foo1(const int &a) { // 加个const防止以后操作的时候不小心改变 //用a接收而不是&a相当于 a = 1000; 形参 值传递了,不是引用了. cout << a << endl; } int main() { int a = 10; foo1(a); }
3. 函数高级
默认参数
如果函数声明时有默认参数,函数实现就不能有默认参数。二者只能有一个有。
void foo1(int a = 1, int b = 2); void foo1(int a=1,int b =2) { cout << a <<" "<<b << endl; } int main() { int a = 10,b=20; foo1(); }
则
函数占位参数
// 目前阶段的展位参数我们还用不到取不到,以后会将。 // 占位参数也有默认参数 void foo1(int a,int=10) {} void foo1(int a,int) { cout << a << endl; } int main() { int a = 10,b=20; foo1(a,b); // 此时必须传两个 }
函数重载
函数名可以相同,提高复用性
void foo(int a) { cout <<"重载函数1 "<< a << endl; } void foo(int a,int b) { cout << "重载函数2 " << a <<" "<< b << endl; } void foo(float a, float b) { cout << "重载函数3 " << a << " " << b << endl; } void foo(int a, float b) { cout << "重载函数4 " << a << " " << b << endl; } int main() { int a = 10,b=20; float c = 1.2, d = 3.14; foo(a); foo(a,b); foo(c,d); foo(a,d); } /* 重载函数1 10 重载函数2 10 20 重载函数3 1.2 3.14 重载函数4 10 3.14 */
注:函数的返回类型返回值不能作为重载条件,看参数就行了。
函数重载注意事项
- 引用作为注意事项
- 有默认值的函数重载
引用作为注意事项
void foo(int &a) { cout <<"重载函数1 "<< a << endl; } void foo(const int &a) { cout << "重载函数2 " << a << endl; } int main() { int a = 10; foo(a); // 重载函数1 10 }
这种情况会走函数1,无论1 2 谁在前。
void foo(int &a) { cout <<"重载函数1 "<< a << endl; } void foo(const int &a) { cout << "重载函数2 " << a << endl; } int main() { int a = 10; foo(10); // 重载函数2 10 }
这种情况直接传10,走函数2。 因为走函数1不合法呗。
有默认值的函数重载
void foo(int a) { cout <<"重载函数1 "<< a << endl; } void foo(int a=99) { cout << "重载函数2 " << a << endl; } int main() { int a = 10; foo(a); }
这样不可以 报错
void foo(int a, int b = 20) { cout <<"重载函数1 "<< a << endl; } void foo(int a=99) { cout << "重载函数2 " << a << endl; } int main() { int a = 10; foo(a); // foo(30,40) 可以 }
也不行 还报错,因为都能走。尽量避免把。
4. 类和对象
C++面向对象三大特征:封装继承多态。
语法 class 类名 {访问权限:属性/行为};
封装
设计一个圆类,显示周长。
#include<iostream> #include<string.h> using namespace std; #define PI 3.1415926 class Circle { public: int r; float calculate() { return 2 * PI * r; } }; int main() { Circle c; c.r = 1; cout << c.calculate() << endl;// 6.28319 }
内部赋值/修改
设计一个学生类(小坑)
#include<iostream> #include<string.h> using namespace std; #define PI 3.1415926 class Student { public: int id; string name; void setS(int id1,string name1) { // 注意 这里不可以写一样的名字C++不认识,Py认识。 id = id1; name = name1; } void show() { cout << id << " " << name << endl; } }; int main() { Student s1; s1.id = 1; s1.name = "张三"; s1.show(); // 1 张三 Student s2; s2.setS(2, "李四"); // 如果写了 id=id name = name 下面就会输出乱码 s2.show(); // 2 李四 }
公有、私有、保护权限
public 类内可以访问 类外可以访问 子类可以访问
private 类内可以访问 类外不可以访问 子类不可以访问
protected 类内可以访问 类外不可以访问 子类可以访问
class与struct
- class默认权限是私有权限。
- struct默认权限是公有权限。
成员属性私有化
这个还有有必要的,我们可以自己控制哪些可读哪些可写。
对于一些写的权限,我们可以进行校验,防止超出有效范围。
include<iostream> #include<string.h> using namespace std; class People { int id; // 只读 string name; // 可读可写 int age; // 可读可写 public: People(int i=000, string n="未实名", int a=18) { // 这里以后讲 先写着玩玩。 id = i; name = n; age = a; } void show() { cout << id <<" "<< name <<" "<< age << endl; } int show_id() { return id; } string show_name() { return name; } int show_age() { return age; } void set_name(string n) { name = n; } void set_age(int a) { if(a<0 || a>150){ cout << "- - gun - -" << endl; return; } age = a; } }; int main() { People p1(001,"张三",20); People p2(002,"李四"); p1.show(); p2.set_age(200); p2.show(); } /* 1 张三 20 - - gun - - 2 李四 18 */
C++不能直接打印出 cout << p << endl;
打印地址 cout << (int * )&p << endl;
对象的初始化和清理
C++利用构造函数和析构函数解决对象的初始化和清理问题。
注:这两个就算你不写,编译器自己调用自己空的。还有个拷贝构造函数也是默认的。也就是说有3个,默认构造函数、析构函数、拷贝构造函数是C++编译器自己添加的。
构造函数
主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
构造函数没有返回值也不写void
函数名称与类名相同
构造函数可以有参数,因此可以发生重载
程序在创建对象的时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数
主要作用在于对象销毁前系统的自动调用,执行一些清理工作。
- 析构函数没有返回值也不写void
- 函数名称与类名相同,在名称前加上~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include<iostream> #include<string.h> using namespace std; class People { public: People() { cout << "你好" << endl; } ~People() { cout << "拜拜" << endl; } }; // 构造和析构都是必须实现的 如果我们不写 编译器会提供一个空实现的构造和析构 int main() { People p1; // 在栈上的数据 main函数执行完就会释放 } /* 你好 拜拜 */
构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
People(const People &p) { // 引用接收 不能把它本身改了呀所以加个const // 这个p是另一个人对象 num = p.num; cout << "拷贝构造函数" << endl; }
注意:构造函数(拷贝)用People &p接收参数可以,People p错(难道你自己写的构造函数构造你自己吗?)。
在其他函数中People p可以是值传递。
调用构造函数
法1:括号法
#include<iostream> #include<string.h> using namespace std; class People { public: int num; People() { cout << "普通:无参构造/默认构造" << endl; } People(int a) { cout << "普通:有参构造" << endl; } People(const People &p) { // 不能把它本身改了呀 // 这个p是另一个人对象 num = p.num; cout << "拷贝构造函数" << endl; } ~People() { cout << "拜拜" << endl; } }; int main() { People p1; // 默认构造函数 People p2(99); // 有参构造函数 People p3(p2); // 拷贝构造函数 此时p3的姓名年龄等与p1一样 } /* 普通:无参构造/默认构造 普通:有参构造 拷贝构造函数 拜拜 拜拜 拜拜 */
注意:调用默认构造函数时,不要加()。python写多了真是不好改。
因为下面这行代码,编译器会认为是一个函数声明。
People p(); // 并不会有什么报错或者输出的对象或者初始化了声明对象
法2:显示法:
int main() { People p1; // 默认构造函数 People p2 = People(99); // 有参构造函数 People p3 = People(p2); // 拷贝构造函数 }
注意:
- People(99); 匿名对象 当前程序结束后,系统会立即回收掉匿名对象。
- 不要用拷贝构造函数初始化匿名对象 People(99) === People p3; 对象重定义。
法3:隐式转换法:
int main() { People p1; // 默认构造函数 People p2 = 99; // 有参构造函数 People p3 = p2 ; // 拷贝构造函数 }
拷贝构造函数调用的三种情况
C++中拷贝构造函数调用时机通常有三种情况
使用一个已经创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值 (People p接收参数值传递相当于给形参赋值;ps任何形式拷贝构造函数中只能以People &p接收)
以值方式返回局部对象 (含有和接收return p返回值时赋值);
情况1:
int main() { People p1; // 默认构造函数 People p2(p1) ; // 拷贝构造函数 }
情况2: 函数用 People p接收
#include<iostream> #include<string.h> using namespace std; class People { public: int num; People() { cout << "普通:无参构造/默认构造" << endl; } People(int a) { num = a; cout << "普通:有参构造" << endl; } People(const People &p) { num = p.num; cout << "拷贝构造函数" << endl; } ~People() { cout << "拜拜" << endl; } }; void foo(People p) { // 值传递 cout << p.num << endl; p.num = 0; cout << p.num << endl; } int main() { People p1(99); cout << p1.num << endl; foo(p1); cout << p1.num << endl; } /* 普通:有参构造 99 拷贝构造函数 99 0 拜拜 99 拜拜 */
情况3: 含有和接收return p返回值时赋值;
#include<iostream> #include<string.h> using namespace std; class People { public: int num; People() { cout << "普通:无参构造/默认构造" << endl; } People(int a) { num = a; cout << "普通:有参构造" << endl; } People(const People& p) { num = p.num; cout << "拷贝构造函数" << endl; } ~People() { cout << "拜拜" << endl; } }; People foo() { // 注意是People 声明函数不止int float等 People p(99); return p; } int main() { foo(); // 匿名对象 直接释放(直接析构) cout << "ok" << endl; //cout << p.num << endl; } /* 普通:有参构造 拷贝构造函数 拜拜 拜拜 ok */
在释放前执行的拷贝构造函数
#include<iostream> #include<string.h> using namespace std; class People { public: int num; People() { cout << "普通:无参构造/默认构造" << endl; } People(int a) { num = a; cout << "普通:有参构造" << endl; } People(const People &p) { num = p.num; cout << "拷贝构造函数" << endl; } ~People() { cout << "拜拜" << endl; } }; People foo() { People p(99); return p; } int main() { People p = foo(); // foo()先拷贝构造函数生成匿名对象 cout << p.num << endl; } /* 普通:有参构造 拷贝构造函数 拜拜 99 拜拜 */
People foo() { People p(99); return p; } int main() { People p = foo(); // 匿名对象 直接释放 cout << "ok" << endl; //cout << p.num << endl; } /* 普通:有参构造 拷贝构造函数 拜拜 ok 拜拜 */
别急 再玩一下:
(上面是值传递 )
引用返回
People &foo() { People p(99); return p; } int main() { People &p = foo(); // 当然了 要是People p = foo(); 就调用拷贝构造函数了,与声明函数时People foo() 无异 cout << p.num << endl; } /* 普通:有参构造 拜拜 99 */
指针
People *foo() { People p(99); return &p; // 这里只能&p 说明对象不像数组一样名字就是地址 } int main() { People *p = foo(); cout << (*p).num << endl; // 这里要加括号 不然报错执行顺序不一样 // cout << p->num << endl; 正常应该这样写 } /* 普通:有参构造 拜拜 99 */
我们可以看到,对于情况3的三种情况:
第一种:People p = foo(); 用p去接收一个对象p`,相当于 People p = p`; 执行拷贝构造函数。
第二种:People &p = foo(); 这个是引用,相当于People &p = p`;给这个起了个别名,不执行拷贝构造函数。
第三种:People *p = foo(); 涉及知识点毕竟较多,上面注释都写了,不执行拷贝构造函数。
2、3种不进行对象与对象之间的赋值,不执行拷贝构造函数。
但是要注意,对于情况三的第2、3种,我这样写只能打印一次,因为说过很多遍了其在栈区用完就清理了,得到的地址没变,里面的值没了。
People* foo() { People p(99); cout << (int*)&p << endl; cout << (int)&p << endl; return &p; } int main() { People* p = foo(); cout << (int*)&*p << endl; cout << (int)&p << endl; } /* 普通:有参构造 00BBFAC8 12319432 拜拜 00BBFAC8 12319672 */
当然你也可以new一块或者static,不过学指针也引用的时候我也写了,不提倡。
所以return回一个对象结构体啥的,因为可能还要用到就别整花里胡哨的直接返回个值就行,当作参数传递给函数可以考虑指针引用节省空间,可以考虑加个const不让他变。
如果这些你都想到了,说明你前面没白学。