1.头文件与类的声明
1.1头文件与源文件
在一个C++程序中,有两种基础的文件:头文件(.h或.hpp)和源文件(.cpp)。两个文件都存放C++的源代码,但各自有不同的作用。头文件通常用于声明类、函数原型、变量声明、宏定义等,而源文件则用于实现这些声明,即定义类的成员函数、函数的实现等。在大型项目中,这种分割管理方式可以提高代码的可读性和可维护性。下面我们通过一个例子来理解这两类文件。
例如,假设我们有一个名为Person
的类,我们可以将这个类的声明放在一个名为Person.h
的头文件中:
// Person.h
#ifndef PERSON_H
#define PERSON_H
class Person {
public:
Person(const std::string& name, int age);
void sayHello() const;
// 其他成员函数和变量的声明...
private:
std::string name_;
int age_;
};
#endif // 防御式声明
然后,在源文件Person.cpp
中,我们实现Person
类的成员函数:
// Person.cpp
#include "Person.h"
#include <iostream>
Person::Person(const std::string& name, int age)
: name_(name), age_(age) {}
void Person::sayHello() const {
std::cout << "Hello, my name is " << name_ << " and I'm " << age_ << " years old." << std::endl;
}
// 其他成员函数的实现...
在大型项目中,这样的分割管理方式使得代码更加清晰、易于维护。当其他部分的代码需要使用Person
类时,只需包含Person.h
头文件即可,而不需要关心类的具体实现细节。同时,这也允许我们单独编译源文件,提高编译效率。
1.2头文件布局
头文件的布局一般包含三个部分:防卫式声明、类-声明、类-定义。
防卫式声明是在前置声明前以及文件最后,作用是如果这个头文件没有被定义,那就定义它,如果已经定义过了,那就忽略。可以防止头文件重复定义。实现方法如图所示,_文件名_
下面是一个标准的头文件(mycomplex.h)为范例,这是一个复数的例子
#ifndef __MYCOMPLEX__
#define __MYCOMPLEX__ //防卫式声明
class complex; //前置声明
complex&
__doapl (complex* ths, const complex& r);
complex&
__doami (complex* ths, const complex& r);
complex&
__doaml (complex* ths, const complex& r);
class complex //类声明
{
public:
complex (double r = 0, double i = 0): re (r), im (i) { }
complex& operator += (const complex&);
complex& operator -= (const complex&);
complex& operator *= (const complex&);
complex& operator /= (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex *, const complex&);
friend complex& __doami (complex *, const complex&);
friend complex& __doaml (complex *, const complex&);
};
#endif //__MYCOMPLEX__
2.内联函数(inline)和访问级别(access level)
2.1 内联函数(inline)
内联函数是一种优化手段,通过在函数声明前加上 inline
关键字来建议编译器在调用该函数时直接将其代码插入到每个调用点,而不是执行常规的函数调用。然而,即使使用了 inline
关键字,编译器也可以选择不内联该函数,这取决于多种因素,如函数的复杂度、大小以及编译器的优化设置。
#include<iostream>
inline const char* oe_judge(const int& num){
return ( num % 2 > 0)? "奇" : "偶";
}
int main()
{
int i = 1;
for(i = 1;i<100;i++)
{
std::cout<<"i:"<<i<<" "<<oe_judge(i)<<"数\n";
}
return 0;
}
在上面的代码片段中,oe_judge
函数被声明为内联函数。如果编译器选择内联它,那么在循环中调用 oe_judge
时,将不会创建函数调用的栈帧,从而节省了栈内存的使用,并可能提升程序的运行效率。然而,这也会导致函数体在代码中的多次复制,从而增加了程序在存储中的大小。
一般来说,建议将内联函数的方法体保持简单,避免使用复杂的循环和开关语句,以提高内联化的可能性。虽然内联函数可能会增加代码段的大小,但在某些情况下,这种“空间换时间”的策略是值得的,因为它可以提高程序的执行效率。
2.2 访问级别(access level)
访问级别分为三类 public 、protected 、private ,具体访问权限如下
- public: 能被所有用户 (类内成员 / 对象 / 友元) 访问
- protected: 能被 派生类成员 / 友元 访问
- private: 能被 类内成员 / 友元 访问
3. 构造函数
3.1 空参、实参、默认参构造函数
构造函数是在一个标准类中必不可少的一部分,分为空参和实参构造以及默认参构造函数,当类的对象被创建的时候,编译系统首先会为该对象分配内存空间,然后自动调用构造函数对成员进行初始化工作。
老师建议采用左侧构造函数,是一种更为专业和效率的写法,变量的数值设定分为两个阶段,第一阶段是初始化,第二阶段是赋值,这里就类似说我们在写代码时,最好是先对变量赋初值,然后再进行操作是最佳的方式。
3.2 构造函数的重载
在现实生产中,我们可能有需求进行不同个数参数的构造,因此对构造函数重载是对现实生产需求的满足。
这里的实部给构造函数real函数1、2代表函数重载,在编译后的函数的实际名称会被编译器改变,所以实现了重载。但是需要注意的是在上面的构造函数重载是不被允许的,原因是两个函数都可以实现无参构造,这样就造成了冲突。
3.3 放在private区中的构造函数
上述的代码中就将构造函数放入了private区中,这样做的目的就是为了不允许外界创建对象。我们为什么要这样做呢?比如说我们对程序的定义中要求只有一个object,例如鼠标只有一个object,外界程序不允许再次创建鼠标对象,但是我们在鼠标类中定义了一个static公有成员,其他函数都可以通过这个来访问类中的方法,那么这样做就实现了对现实的生产需求。
3.4 常量成员函数的要求
在上面这个代码片段中,老师特别强调的一点是我们要在写函数时想清楚是否要对参数的数值发生改变,如果不改变那么就要加上const这个标识符,比如说右侧的例子中如果一开始就设置为常量,但是在你的函数中将数值改变了的话,就会产生bug!
4.函数参数传递的两种方式
4.1 值传递(pass by value)
值传递直白地说就是传值,比如说上面的带参构造c1,值传递过程中,在堆栈中开辟了形参内存空间,并调用拷贝构造函数把实参值复制给形参,从而成为了实参的一个脚本。值传递的效率很低且占用堆栈空间,比如说我们如果在值传递中参数是一个超大的字符串,那么就会极大影响运行效率和空间。因此这种方式不太推荐使用。
4.2 引用传递(pass by reference)
引用传递就是指针传递,引用传递 (pass-by-reference)过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址,因此相比值传递使用空间减少了很多,当然如果值传递的参数如果仅为1个字节的char类型数据,那么空间上引用传递就会多出几个字节。在引用传递中我们特别需要注意的是const修饰符的使用,避免数据修改问题从而导致后续函数操作出现相关问题。
在函数值的返回上我们同样也可以使用引用传递,但是要注意现实生产的需求(结合拷贝开销问题)。