C++学习杂记(二)


今日学习内容

继续学习C++的相关知识。


一、C++类成员的访问限制

控制成员变量和成员函数的访问权限关键字有3个:
(1)public——共有的,类的内部和外部均可以访问。
(2)protected——类的外部对象不能访问,按该类的派生类可以访问。
(3)private——只能在类的内部被访问。
注意:
a. 以上3个关键字只能修饰类的成员,不能修饰类,在C++中类是没有共有私有之分的。
b.当类的内部成员无论被哪个关键字修饰时,定义类的内部代码都可以访问该成员,没有访问限制;但在类的外部代码想访问类的内部成员时,只能访问public属性的成员,不能访问private、protected属性的成员,而且必须通过类的对象进行访问。

#include <iostream>

using namespace std;

//定义一个类
class People{
private:  //私有的,只能类的内部代码进行访问,类的代码外部没权限访问
	char *m_name;
	int m_age;

public: //共有的,类外面的代码可以通过类的对象进行直接访问
	void say(char *name,int age); //类的成员函数声明
	void show();
}; //定义类时,不要漏了这个分号,分号表示类的定义结束。

//类的成员函数定义
void People::say(char *name,int age){
	m_name = name;
	m_age = age;
}
void People::show()
{
	cout<<m_name<<" is "<<m_age<<" years old."<<endl;
}

void main()
{
	//创建People类的对象,这种创建方式是在栈上创建的
	People LiMing; //也可以写成 class People LiMing;但我们一般都是将class关键字省略。
	//通过类的对象访问成员函数,仅仅能访问public的函数,类成员是private,不能直接访问。
	LiMing.say("LiMing",18);
	LiMing.show();
	
	//通过关键字new来创建对象,这种方式是在堆上创建的,new和delete配对使用。
	People *XiaoPeng = new People;
	XiaoPeng->say("XiaoMing",22);
	XiaoPeng->show();
	//XiaoPeng->age = 10; //如果写这句,编译就会报错:“People::age”: 无法访问 private 成员(在“People”类中声明。
	
	//通过sizeof来判断对象的内存占用大小
	cout<<"通过在栈上创建的对象内存为:"<<sizeof(LiMing)<<endl;
	cout<<"通过在堆上创建的对象内存为:"<<sizeof(*XiaoPeng)<<endl;
	cout<<"类的大小:"<<sizeof(People)<<endl;
}

运行结果:
在这里插入图片描述
注意:a. 一般在开发中,我们将类的声明放在头文件中,将类的成员函数放在源文件中。
b. 一般将成员变量命名做一个表示,在前面加一个m_,这个不是语法规定,只是编码习惯。
c.在类中声明public和private的顺序是随意的,也可以多次使用关键字。但如果都不显式写public和private,就会默认为private。

二、类的封装

当我们不想暴露类的内部实现,就可以用private修饰成员变量和成员函数,但如果类的对象想访问的成员就可以用public修饰。
封装:是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。
**问题1.**由于类在定义时,类是不占内存的,如果被private修饰了,类的对象又不能访问,那么该如何给成员变量赋值呢?
**答:**通过写一个public修饰的对外暴露的成员函数,比如上面的代码中say函数,就给m_name和m_age两个成员变量赋值。

三、C++对象的内存模型

1.前面已经提到过,类是创建对象的模板,是不占用内存空间,不存在于编译后的可执行文件中。而类创建的对象是需要在栈或者堆内分配内存的。通过类直接创建的成员变量是在栈中,通过new关键字创建的对象其成员变量是在堆中,成员函数在代码区分配内存。
2.在多个对象时,由于每个对象的成员变量的值不同,所以会有多块内存存放;但不同对象的成员函数是共享的,所以成员函数只存一块内存。
在这里插入图片描述
从上述代码中可以看出,sizeof(对象)的大小只包含了成员变量,不包含成员函数。因此,对象的大小只受成员变量的影响,和成员函数没有关系。
在这里插入图片描述
内存的排布与类的声明中成员变量的顺序有关,这点和结构体类似,且存在字节对齐的问题。

四、C++函数编译原理浅析

1.C++的编译方式和C语言不同,C语言编译的过程中,函数的名字不会改变,可能是在函数名前加“_”(具体看编译器如何实现)。
2.C++中的函数在编译时会根据它所在的命名空间、它所属的类、以及它的参数列表(也叫参数签名)等信息进行重新命名,形成一个新的函数名。
3.C++的这种函数编译正常时,这种命名方式是对用户不可见的。但编译报错时,就可以看到这个新的函数命名,从而可以看出是哪个函数编译报错。
示例一段代码。声明两个全局函数(函数重载了),一个成员函数,但都未定义,编译报错。

#include <iostream>
using namespace std;

void show();
void show(int n);

class People{
private:
    char *m_name;
    int m_age;
public: 
	void show();
}; 

void main()
{
	show();
	show(25);
	People Li;
	Li.show();
}

编译报错如下:

1>  main.cpp
1>main.obj : error LNK2019: 无法解析的外部符号 "public: void __thiscall People::show(void)" (?show@People@@QAEXXZ),该符号在函数 _main 中被引用
1>main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl show(int)" (?show@@YAXH@Z),该符号在函数 _main 中被引用
1>main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl show(void)" (?show@@YAXXZ),该符号在函数 _main 中被引用
1>D:\visual studio 2010\Projects\Debug\test.exe : fatal error LNK1120: 3 个无法解析的外部命令

从报错中可以看出,不仅仅是函数名会在编译阶段重写命名,变量也会重写命名。

五、C++中的成员函数实现

1.从上图可以看出,成员函数最终被编译成与对象无关的全局函数,如果函数体中没有成员变量,不用对函数做任何处理,直接调用即可。但当类的成员函数中访问了类的成员变量时,就需要将当前对象的指针传递进去,通过指针访问成员变量。

//类的成员函数的源代码:
void People::show(){
    cout<<m_age<<endl;
}
//编译后等价于如下:自动传递了一个对象的指针。
void new_function_name(People *const p){
    cout<<p->m_age<<endl;
}

注意:const表明该指针指向的值不能被修改,也就是只能指向当前对象。

六、C++中的构造函数

前面提到过,类的成员变量不能在类定义时赋值,可以通过对外暴露的public修饰的成员函数对成员变量赋值。除了这种方法,还可以通过构造函数对成员变量进行赋值。
1.构造函数的特点:
a.构造函数的函数名与类名相同,没有返回值;
b.也是属性成员函数,但不能被用户调用,而是在对象创建时自动执行。
c.通过构造函数可以给成员变量赋值。
注意:
a. 调用构造函数,就得在创建对象的同时传递实参,并且实参由( )包围。
b. 构造函数必须是 public 属性的,否则创建对象时无法调用。如果设置为 private、protected 属性也不会报错,但是没有意义。
c. 构造函数没有返回值。所以不管是声明还是定义,构造函数名前面都不能出现返回值类型,void也不行。构造函数的函数体中不能有return语句。
2.构造函数的重载
一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。
注意:
a. 构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。
b. 创建对象时只有一个构造函数会被调用。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配。
c. 如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。调用没有参数的构造函数也可以省略括号。
d. 一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。

#include <iostream>
using namespace std;
//类的声明
class People{
private:
    char *m_name;
    int m_age;
public:
	//显示声明默认构造函数
    People();
	//声明一个重载的构造器
    People(char *name, int age);
	//成员函数的声明
    void setname(char *name);
    void setage(int age);
    void show();
}; //类的定义结束

//注意:构造函数的函数名与类名一致,并且是没有返回类型的,void也不能有,函数体内不能有return语句。都声明为public的。
//这也是区分成员函数和构造函数的标志。
//显示定义默认的构造函数
People::People(){
    m_name = NULL;
    m_age = 0;
}
//重载一个带参数的构造函数,可以直接对成员变量初始化。该构造器在调用时,需要对象直接给出实参
People::People(char *name, int age){
    m_name = name;
    m_age = age;
}

//成员函数的定义
void People::setname(char *name){
    m_name = name;
}
void People::setage(int age){
    m_age = age;
}
void People::show(){
    if(m_name == NULL || m_age <= 0){
        cout<<"成员变量还未初始化"<<endl;
    }else{
        cout<<m_name<<"的年龄是"<<m_age<<endl;
    }
}

int main(){
    //调用带参数的构造函数 People(char *, int, float)
    People stu("小明", 15);
    stu.show();
    //调用构造函数 People(),由于该构造函数未初始化,可以再调用public类型的set成员函数赋值。
    People *pstu = new People(); //由于调用的构造函数不带参数,可以将()省略,写成: People *pstu = new People;
    pstu -> show();
    pstu -> setname("李华");
    pstu -> setage(28);
    pstu -> show();
    return 0;
}

打印结果:
在这里插入图片描述
3.构造函数的初始化列表
利用初始化列表,可以简化书写方式,不会带来效率上的提升。

//构造函数常规写法
People::People(char *name, int age){
    m_name = name;
    m_age = age;
}
//采用初始化列表的方法
People::People(char *name, int age):m_name(name),m_age(age){
   //函数体内可以空着了,也可以有其他操作的语句。
}
//也可以符部分采用初始化列表初始化,其他采用赋值语句。
People::People(char *name, int age):m_name(name){
    m_age = age;
}

注意:
成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。

#include <iostream>
using namespace std;
class Demo{
private:
    int m_a;
    int m_b;
public:
    Demo(int b); //含有一个入参的构造函数声明
    void show();
};
Demo::Demo(int b): m_b(b), m_a(m_b){ }
void Demo::show()
{ 
	cout<<m_a<<", "<<m_b<<endl;
}
int main(){
	int ages;
    Demo obj(10);
    obj.show();
	cin>>ages;
    return 0;
}
//调用构造函数后的两个成员初始化的结果为:m_a = 随机值,m_b = 10;
//原因是初始化只与类声明的成员变量顺序有关。
//构造函数等价如下:
Demo::Demo(int b):{
	//按照类的成员变量定义的顺序进行初始化,由于m_b是未知的脏值,随意产生的是随机值,m_b则等于b = 10;
	m_a = m_b;
	m_b = b;
}

打印结果:
在这里插入图片描述
4.初始化const成员变量
构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。
注意:初始化 const 成员变量的唯一方法就是使用初始化列表。不能使用常规的构造函数进行赋值。

//定义一个模拟变长数组的类VLA
class VLA{
private:
    const int m_len; //注意被const修饰了
    int *m_arr;
public:
    //构造函数
    VLA(int len);
};
//必须使用初始化列表来初始化 m_len
VLA::VLA(int len): m_len(len){
    m_arr = new int[len];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值