一、结构体
1.结构体的定义
结构体(struct)是一种用户可以自定义的复合数据类型,可以包含不同类型的多个不同成员。
语法:
struct 结构体名
{
成员1的数据类型 成员1的名称;
成员2的数据类型 成员2的名称;
···
};
例:
struct Student
{
string name;
int age;
string gender;
};
此时我们就创建出了Student这一复合数据类型并可以使用这一数据类型了
//结构体变量的声明,可以在前面带上struct关键字也可以不带
//但为了清晰的认知,建议写上struct
struct Student stu1; //声明stu1这一结构体变量
stu1 ={ "顶针" , 24 , "男" }; //为结构体变量的每一个成员进行赋值
由于结构体变量是一个整体的包装,无法直接cout输出,我们需要访问它的每一个成员来进行输出
访问语法: 结构体变量名.成员名称
//错误写法
cout<<stu1<<endl; //执行不了
//正确写法
cout << "姓名:" << stu1.name << endl;
cout << "年龄:" << stu1.age << endl;
cout << "性别:" << stu1.gender << endl;
2.结构体默认值的设置
在设计结构体时,可以根据需要向成员设置默认值
例:
struct Student
{
string name;
int age = 18; //默认年龄为18
string gender = "未知"; //默认性别为未知
};
当声明一个新结构体变量但未给它的成员赋值时,那么它的成员就变为结构体的默认值。
3.结构体指针
作为一种数据类型,结构体也是支持使用指针的。
但在使用指针访问结构体成员时需要更换操作符号为 : ->
例:
struct Student * p = new Student { "悟空" , 20 , "男" };
cout << "姓名:" << p->name << endl;
cout << "年龄:" << p->age << endl;
cout<<"性别:" << p->gender << endl;
当结构体变量为数组时,同样也可以使用指针:
struct Student * p = new Student [3]{
{"顶针",24,"男"},
{"hx",20,"女"},
{"qhl",20,"男"}
}; //创建结构体数组,有三个成员
cout<<p[0].name<<endl; //第一个成员的姓名
cout<<p[1].name<<endl; //第二个成员的姓名
cout<<p[2].name<<endl; //第三个成员的姓名
二、函数
1.函数的定义
函数(Function):指的是一个提前封装好的、可多次调用的、通过用户设计能完成特定功能的独立代码单元。它包含了一系列的代码来实现某种功能。
语法:
数据类型 函数名 (参数1,参数2,···) //参数数量不作规定
{
功能代码;
··· //函数体,
返回值; //返回值即函数得到的结果,它必须与函数的数据类型相同
}
例:
//编写一个函数,能够返回3个值中的最大值
int Max(int a,int b,int c)
{
int max=a;
if(b>max)
{
max=b;
}
if(c>max)
{
max=c;
}
return max;
}
如此,一个求最大值的函数就完成了。
函数的返回值也可以是一个指针(内存地址)。
2.void类型函数和无参函数
函数的返回值并非是必须提供的,也就是说声明函数时可以不提供返回值
当函数不提供返回值时,需要:
·声明函数返回值类型为:void (void表示空的含义)
·不需要写return语句
·调用者无法得到返回值
例:
void say_hello(string name)
{
cout<<"大家好,我是"<<name<<endl;
}
函数的传入参数也是可选的,即声明不需要参数(形参)的传入。(注意!!”()“必须写)
例:
void i_like_u()
{
cout<<"我喜欢你!"<<endl;
}
3.函数的嵌套调用
函数作为一个独立的代码单元,也可以函数内调用其他函数。
这种嵌套调用关系没有任何限制,可以根据需要来设计嵌套层数。
例:
int func_a(){code;}
int func_b(){code;}
int func_A()
{
code;
if(···)
{func_a();}
if(···)
{func_b();}
code;
}//在func_A函数内调用func_a和func_b函数
4.函数参数的值传递和地址传递
(1)值传递
前面学习的函数的形参声明,其实都是在使用”值传递“的参数传递方式,而在某些情况,这种传递方法使我们在使用函数后不能得到想要的结果
例:
//值传递
void swap(int a,int b) //交换a,b的值
{
int temp=a;
a=b;
b=temp;
}
int main()
{
int x=10,y=20;
cout<<"交换前:x="<<x<<" y="<<y<<endl; //交换前结果:x=10,y=20
swap(x,y);
cout<<"交换后:x="<<x<<" y="<<y<<endl; //交换后结果:x=10,y=20
return 0;
}
此时发现值传递这种方法在使用函数后得到的结果并不是我们所预期的。
因为值传递本质上是将x和y的值分别赋值给了函数内的形参a和b,发生交换的实际上是函数内的a和b,而x和y并没有发生交换。
(2)地址传递
下面我们换为地址传递的方法:即把形参变为指针变量的形式
//地址传递
void swap(int *a,int *b) //交换a,b的值
{
int temp=*a;
*a=*b;
*b=temp;
}
int main()
{
int x=10,y=20;
cout<<"交换前:x="<<x<<" y="<<y<<endl; //交换前结果:x=10,y=20
swap(&x,&y);
cout<<"交换后:x="<<x<<" y="<<y<<endl; //交换后结果:x=20,y=10
return 0;
}
地址(指针)转递这种方法是直接作用于内存上,所以可以交换x和y的值,得到我们想要的交换效果
除了值传递和地址传递,后边还会学到一种常用的引用传递。
(3)函数传入数组
前面我们提到,数组v的本身存储的只是第一个元素v[0]的地址,所以当数组作为传参时,本质上都是地址传递:
void func(int arr[]){}
void func(int arr[10]){}
void func(int * arr){}
//以上三种写法其实效果都是完全一致的,形参实际上都是地址,完全等同于传递指针
例:
void func(int arr[])
{
cout<<"func函数内统计的数组总大小为"<<sizeof(arr)<<endl;
}
int main()
{
int arr[5]={1,2,3,4,5};
cout<<"main函数内统计的数组总大小为"<<sizeof(arr)<<endl;
func(arr);
system ("pause");
return 0;
}
//输出结果为:main函数内统计的数组总大小为20
// func函数内统计的数组总大小为4 (32位系统的指针大小为4字节,64位为8字节)
由上可以看出,func的参数为数组时实际上就是一个指针。
例题:编写一个函数,能接收数组的传入,并对数组进行升序排序
void sort_arr(int arr[],int length) // length指数组的长度
{
int min , min_index ; //min记录循环时得到的最小值,min_index记录最小值的下标
for(int i = 0; i < length-1; i++) //外层循环控制循环次数
{
for(int j = i; j< length;j++) //内层循环寻找最小值并排序
{
if(j==i) //本次内层循环的第一个元素
{
min = arr[j];
min_index = j;
}
if(arr[j]<min)
{
min = arr[j];
min_index = j;
}
}
int temp = arr[i];
arr[i] = arr[min_index];
arr[min_index] = temp; //交换
}
}
int main()
{
int arr[5]={150 , 21 , 335 , 45 , 54 };
sort_arr( arr , 5 );
cout<<"排序后的数组为:";
for(int i = 0; i < 5; i++)
{
cout<<arr[i]<<" ";
}
return 0;
}
三、引用
1. 引用的定义
引用 也就是给变量起一个别名,它是某个已存在的变量的另一个名字。我们可以通过这个别名来访问这个变量指向的内存区域,它的主要使用符号为“ & ”
语法:
数据类型& 引用名(别名) = 被引用变量;
//例:
int a = 10;
int& b = a;
它的特点与之前学过的const指针(常量指针)类似,不可以更改指向的区域,因为不可以更改指向区域,所以引用不可以为空,必须初始化。 !!引用 ≠ 指针 !!
2.引用传递
值传递会复制值,无法对实参本身产生影响;
地址传递会复制地址,对实参本身可以产生影响,但指针的写法麻烦;
引用传递可以像普通变量那样操作,也可以对实参本身产生影响。
例:
//引用传递
void swap(int &a,int &b) //交换a,b的值
{
int temp=a;
a=b;
b=temp;
}
如此以来,这个函数也可以实现对实参的值的交换功能。
四、函数的变量生命周期问题
1.内存分区模型
C++在执行程序时,将内存大致分为4个区域:
···代码区:存放程序员编写的所有代码,由操偶做系统进行管理
···全局区:存放全局变量和静态变量以及常量
···栈区:存放函数的参数值,局部变量等,由编译器自动分配和释放
···堆区:由程序员自行分配和释放,若不释放,程序结束后由操作系统自动回收
内存四区的意义:
不同区域存放的数据,具有不同的生命周期,使我们编程时管理数据更方便
(1)程序运行前
在程序编译后,会生成一个exe可执行程序,未执行该程序前分为两个区域
代码区:
存放CPU执行的机器指令(程序员写的代码)。
代码区是共享的,对于频繁执行的程序,只需要在内存中共享同一份代码即可。
代码是只读的,目的是为了防止程序意外地修改了它的指令。
全局区:
存放全局变量和静态变量。
还包含了常量区,用于存放字符串常量和其他常量。
该区域的数据在程序结束后由操作系统释放。
全局变量:不在函数体中声明的变量
局部变量:在函数体中声明的变量
int ga=100, gb=200, gc=300; //不在任何函数体中的全局变量
int main()
{
int a = 10, b = 20 , c = 30; //在主函数中的局部变量
cout<<"a 的地址 "<<&a<<endl;
cout<<"b 的地址 "<<&b<<endl;
cout<<"c 的地址 "<<&c<<endl;
cout<<"ga 的地址 "<<&ga<<endl;
cout<<"gb 的地址 "<<&gb<<endl;
cout<<"gc 的地址 "<<&gc<<endl;
return 0;
}
结果输出:
显然可以看出,全局变量和局部变量存放的地址不在同一块上
(2)程序运行后
栈区:
由编译器自动分配释放,存放函数的参数值、局部变量等。
由前面的学习我们已经知道,由于栈区的数据由编译器自动释放,所以不能返回局部变量的地址。
堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收。
在C++中我们主要用new在堆区开辟内存,在之前的学习中我们已经学过并举例。
函数的返回值可以是一个指针,语法如下:
int *add (int a, int b) //实现a,b两数相加
{
int sum;
sum = a + b;
return ∑ //返回sum的地址
}
以上代码的语法是正确的可以运行,但是使用上无法正常返回结果,这是因为sum这个变量是函数内的一个局部变量,由于C++的静态内存分配管理,它的作用范围只在函数内,也就是说当函数执行完毕后,sum的内存空间将被释放,也就返回不了我们想要的结果。
2.使用动态内存管理(new和delete)解决
为了规避这一问题,我们可以自行动态内存管理(new和delete)
int * add(int a , int b)
{
int * sum = new int; //为sum手动分配内存空间
* sum = a + b ;
return sum;
}
int main()
{
int * result = add(1 , 2);
cout<<"结果:"<< * result <<endl;
delete result; //结束后别忘了释放内存
return 0;
}
3.使用static关键字
static这个单词是静态的意思,可以修饰变量和函数。
当static修饰变量时,这个变量就会被存入静态内存区域(后续学习),这个变量的生命周期就可以延长至整个程序运行周期。
int * add(int a , int b)
{
static int sum; //用static修饰sum,sum将持续存在直到程序结束
sum = a + b ;
return ∑
}
int main()
{
int * result = add(1 , 2);
cout<<"结果:"<< * result <<endl; //结果:3
return 0;
}
至此,C++的基础入门阶段的学习已经完成,接下来将进入下一阶段C++核心编程——面向对象编程技术的学习。
其他内容