c++类和函数

内联函数、重载函数

在调用一个函数的时候,需要首先要把参数放到栈中, 返回地址也要放到栈中。 这个函数执行完返回以后,要从栈中取出返回地址,再跳转到返回地址去执行。

内联函数

  • 函数调用是有时间开销的。如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。
  • 为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。

定义内联函数时,在定义前面加上inline关键字。

inline int Max(int a, int b){
if(a > b) return a;
return b;
}

内联成员函数

其定义方式可以为:

  • inline+成员函数
  • 在类定义内部定义整个函数体
class B{
inline void func1(); // 第一种方式
void func2(){}; // 第二种方式
};
void B::func1(){}

函数重载

一个或多个函数,名字相同,参数个数或参数类型相同,叫做函数的重载。
如下面函数为重载函数:

int Max(double f1, double f2){}
Max(3.4, 2.5);

int Max(int n1, int n2){}
Max(2, 4);

int Max(int n1, int n2, int n3){}
Max(1, 2, 3)

Max(3, 2.4) // 编译错误(二义性)

其优点是:

  • 简化函数命名规则
  • 编译器只需要根据语句中的实参个数和类型判断应调用的函数

成员函数的重载

#include <iostream>
using namespace std;
class Location{
private:
int x, y;
public:
void init(int x = 0, int y = 0);
void valueX(int val){
x = val;
}
int valueX(){
return x;
}
};

valueX()为成员函数重载,使用时仍然可以参数缺省。

void Location::init(int X, int Y){
x = X;
y = Y
}
int main(){
Location A;
A.init(3); // 将x的值初始化为3,y缺省为0
A.valueX(5); // 调用赋值接口,将x的值变为5
cout<<A.valueX(); // 调用get接口获取x的值,输出5
return 0;
}

函数缺省

C++中,定义函数时,可以让最右边的连续若干个参数有缺省值,调用函数时,若相应位置不写参数,参数就是缺省值。 如:

void func(int x1, int x2 = 2, int x3 = 3){}

func(10); // 等效于func(10, 2, 3)
func(10, 8); // 等效于func(10, 8, 3)
func(10, , 8); // 编译错误,缺省参数只能是在最右边的连续参数

其优点为:

  • 函数参数可缺省的目的在于提高程序的可扩充性。
  • 如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。

成员函数缺省

使用缺省参数时,需要避免函数重载时的二义性。

class Location{
private:
int x, y;
public:
void init(int x = 0, int y = 0);
void valueX(int val = 0){
x = val;
}
int valueX(){
return x;
}
};

使用时:

int main(){
Location A;
A.valueX(); // 此时会编译错误,报错为产生二义性
}

程序设计方法

类基础

类定义变量的过程为实例化。

class CRectangle{
public:
int w, h;
void init(int w_, int h_){
w = w_;
h = h_;
}
int area(){
return w * h;
}
int perimeter(){
return 2 * (w + h);
}
}; // ;必须有

对象之间可以用 ‘=’ 进行赋值 ,不能用 ‘==’, ‘!=’, ‘>’, ‘<’, ‘>=’, ‘<=’进行比较 ,除非运算符进行重载

访问类成员变量

  1. 对象名.成员名
CRectangle r1, r2;
r1.w = 5; // 每个对象有各自的存储空间
r2.init(3, 4);
  1. 指针->成员名
CRectangle r1, r2;
CRectangle *p1 = & r1;
CRectangle *p2 = & r2;
p1->w = 5;
p2->inti(3, 4); // init作用在p2指向的对象上
  1. 引用名.成员名
CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.init(3, 4) // /rr的值变了,r2的值也变

类的成员函数

成员函数体和类的定义可以分开写:

class CRectangle{
public:
int w, t;
// 成员函数在这里声明
int area();
int perimeter();
void init(int w_, int h_);
};


// 函数定义可以在类外
int CRectangle::area(){
return w * h;
}
int CRectangle::perimeter(){
return 2 * (w + h);
}
void CRectangle::init(int w_, int h_){
w = w_;
h = h_;
}

类的构造函数

类的构造函数是成员函数的一种,但需要遵循特殊的规律:

  • 名字与类名相同,可以有参数,不能有返回值(void也不行)
  • 其作用是初始化对象,比如给成员变量赋初始值
    • 对象生成时自动调用构造函数,对象一旦生成,就无法再执行构造函数
    • 一个类可以有多个构造函数
  • 如果定义类时未写构造函数,编译器自动生成一个无参数的构造函数,不做任何操作;
  • 如果定义类时定义了构造函数,编译器不生成无参数构造函数
构造函数作用
  1. 执行必要的初始化工作
  2. 有时对象没被初始化就使用,会导致程序出错
class Complex{
private:
doubel real, imag;
public:
void Set(double r, double i);
};
// 编译器自动生成默认构造函数

Complex c1; // 默认构造函数被调用
Complex *pc = new Complex; // 默认构造函数被调用

编写构造函数时:

class Complex{
private:
double real, imag;
public:
Complex(double r, double i = 0);
};
Complex::Complex(double r, double i){
real = r;
imag = i;
}

Complex c1; // error, 缺少构造函数的参数
Complex *pc = new Complex; // error, 缺少构造函数的参数

Complex c2(2); // 构造函数重载
Complex c2(2, 4), c3(3, 7);
Complex *pc = new Complex(5, 9);

有多个构造函数的情况:多个构造函数重载

class Complex { 
private:
double real, imag;
public:
void Set( double r, double i );
Complex(double r, double i );
Complex(double r );
Complex(Complex c1, Complex c2);
};
Complex::Complex(double r, double i) { // 构造函数1
real = r;
imag = i;
}
Complex::Complex(double r) { // 构造函数2
real = r;
imag = 0;
}
Complex::Complex (Complex c1, Complex c2){ // 构造函数3
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}

实例化时:

Complex c1(3);         // 调用构造函数2,整型可以自动转换为double类型
Complex c2(1, 0); // 调用构造函数1
Complex c3(c1, c2); // 调用构造函数3

构造函数最好是public的,private构造函数不能直接用来初始化对象。

构造函数在数组中的使用
class CSample{
int x;
public:
CSample(){
cout << "Constructor 1 Called" << endl;
}
CSample(int n) {
x = n;
cout << "Constructor 2 Called" << endl;
}
};

实例化时:

int main(){
CSample array1[2];
cout << "step1"<<endl;

CSample array2[2] = {4,5};
cout << "step2"<<endl;

CSample array3[2] = {3};
cout << "step3"<<endl;

CSample * array4 = new CSample[2];
delete []array4;

return 0;
}

输出结果为:

Constructor 1 Called
Constructor 1 Called
step1
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called

另一个例子:

class Test{
public:
Test(int n){ } // (1)
Test(int n, int m){ } // (2)
Test(){ } // (3)
};
Test array1[3] = {1, Test(1,2)};
// 三个元素分别用(1),(2),(3)初始化
Test array2[3] = {Test(2,3), Test(1,2), 1};
// 三个元素分别用(2),(2),(1)初始化
Test *pArray[3] = {new Test(4), new Test(1,2)};
//两个元素分别用(1),(2) 初始化

第三个定义了一个指针数组,不会引发Test构造函数被调用,因为其每个元素都是指针,并不是对象,因此没有进行实例化。对于这个指针数组,可以不进行初始化操作。当new出来两个对象后,new表达式的返回值为Test *类型的指针。该表达式只生成了两个对象,而不是三个。pArray[2]是一个未经初始化的指针。

复制构造函数

复制构造函数(copy constructor)只有一个参数,即对同类对象的引用,不能直接为同类的对象。

形如:

X::X(X &)X::X(const X &),二者选一。后者能以常量对象作为参数。

若未定义复制构造函数,编译器生成默认复制构造函数,默认复制构造函数完成复制功能。

例:

class Complex{
private:
double real, imag;
};
Complex c1; // 调用缺省无参构造函数
Complex c2(c1); // 调用缺省的复制构造函数,将c2初始化为和c1相同

当定义了复制构造函数之后,编译器不会自动生成复制构造函数。

class Complex{
public:
double real, imag;
Complex(){}
Complex(const Complex & c){
real = c.real;
imag = c.imag;
cout<<"Copy constructor called";
}
};
Complex c1; // 直接调用第一个构造函数
Complex c2(c1); // 初始化c2,调用自定义的复制构造函数
作用
  1. 当用一个对象去初始化同类的另一个对象时。

如上面例子中用c1初始化c2。其中Complex c2 = c1也是初始化语句,等价于Complex c2(c1),是非赋值语句。

  1. 如果某函数有一个参数是类 A 的对象,该函数被调用时,类A的复制构造函数被调用
class A{
public:
A(){};
A(A & a){ // 自定义的复制构造函数,未进行复制工作
cout<<"Copy constructor called";
}
}
void Func(A a1){} // 形参a1使用自定义复制构造函数初始化
int main(){
A a2;
Func(a2); // 调用该函数时,调用A的复制构造函数
return 0;
}

此时的形参a1和实参a2的值可能不相同,因为自定义的复制构造函数没有进行复制工作。当未自定义复制构造函数时,形参a1的值和实参a2的值是相等的,因为编译器自动生成的复制构造函数会进行复制工作。
输出结果:

Copy constructor called
  1. 如果函数的返回值是类A的对象,则函数返回时,A的复制构造函数被调用
class A{
public:
int v;
A(int n){v = n;}
A(const A & a){
v = a.v;
cout<<"Copy constructor called";
}
};
A Func(){
A b(4);
return b;
}
int main(){
cout<<Func().v<<endl;
return 0;
}

调用Func()函数时,首先需要生成临时返回值对象,调用复制构造函数,初始化临时返回对象,形参为b,因此输出“Copy constructor called”,且这里复制构造函数对v的值进行了复制操作,因此输出“4”。因此函数的返回值可能和b不是相等的,这取决于复制构造函数的写法。
输出结果:

Copy constructor called
4

类型转换构造函数

类型转换构造函数的目的是实现类型的自动转化。
特点:

  • 只有一个参数
  • 不是复制构造函数
  • 编译系统自动调用转换构造函数同时创建临时变量

例:

class Complex{
public:
double real, imag;
Complex(int i){ // 类型转换构造函数
cout<<"IntConstructor called"<<endl;
real = i;
imag = 0;
}
Complex(double r, double i){ // 传统构造函数
real = r;
imag = i;
}
};
int main(){
Complex c1(7, 8); // 调用传统构造函数初始化c1
Complex c2 = 12; // 初始化c2,调用类型转换构造函数
c1 = 9; // 赋值语句,编译器自动调用类型转换构造函数,此时已9作为实参,调用类型转换构造函数,生成临时对象,赋值给c1
cout<<c1.real<<","<<c1.imag<<endl;
return 0;
}

输出:

IntConstructor called
IntConstructor called
9,0

析构函数

  • 析构函数是成员函数的一种
  • 名字与类名相同,形如~X()
  • 没有参数和返回值
  • 一个类最多只有一个析构函数

析构函数在对象消亡时会自动被调用,释放给类分配的对象空间。未定义析构函数时,编译器会自动生成缺省析构函数,但该析构函数不涉及释放用户申请的内存释放等清理工作。当自定义析构函数时,编译器不会生成缺省构造函数。

class String{
private:
char * p;
public:
String(){
p = new char[10];
}
~String();
};
String::~String(){
delete [] p; // 自定义析构函数
}

在初始化构造函数时,new了一个char型数组,分配了一个空间。调用析构函数时,需要将new出来的空间delete掉。

析构函数和数组

在对象数组生命周期结束时,对象数组的每一个元素的析构函数都会被调用。

class Ctest{
public:
~Ctest(){
cout<<"Destructor called"<<endl;
}
};
int main(){
Ctest array[2];
cout<<"End Main"<<endl;
return 0;
}

在程序结束之前,编译器会调用析构函数,因为是两个对象,因此会执行两次析构函数,最后输出为:

End Main
Destructor called
Destructor called
delete运算符

delete运算符会导致析构函数的调用

Ctest * pTest;
pTest = new Ctest; // 构造函数调用
delete pTest; // 析构函数调用

// 数组delete运算符
pTest = new Ctest[3]; // 构造函数调用3次
delete [] pTest; // 析构函数调用3次

构造函数和析构函数调用例子

定义一个Demo类,并定义构造函数和析构函数。

class Demo{
int id;
public:
Demo(int i){
id = i;
cout<<"id="<<id<<"Constructed"<<endl;
}
~Demo(){
cout<<"id="<<id<<"Destructed"<<endl;
}
};
Demo d1(1);      // 定义全局变量Demo对象,调用构造函数初始化输出“id=1 Constructed”
void Func(){
static Demo d2(2); // 定义静态局部变量,调用构造函数初始化d2,输出“id=2 Constructed”,静态变量消亡是在程序结束时,因此出了Func函数后仍然不会调用d2的析构函数
Demo d3(3); // 定义d3局部变量,调用构造函数输出“id=3 Constructed”,在出Func函数,即其作用域时,调用析构函数,输出“id=3 Destructed”
cout << “Func” << endl;
}
int main (){
Demo d4(4); // 局部变量Demo对象,调用构造函数初始化输出“id=4 Constructed”
d4 = 6; // 调用类型转换构造函数,需要生成临时Demo对象,输出“id=6 Constructed”,赋值完成后需要调用析构函数销毁临时Demo对象,因此输出“id=6 Destructed”
cout << “main” << endl;
{ Demo d5(5);} // 添加自身作用域,表示d5只在作用域中存在,因此需要调用构造函数,输出“id=5 Constructed”,在作用域结束时,调用析构函数,输出“id=5 Destructed”
Func(); // 进入Func函数中
cout << “main ends” << endl;
return 0;
}

在main函数结束后,会依次析构局部变量、局部静态变量、全局变量。
最终的输出结果为:

id=1 Constructed
id=4 Constructed
id=6 Constructed
id=6 Destructed
main
id=5 Constructed
id=5 Destructed
id=2 Constructed
id=3 Constructed
Func
id=3 Destructed
main ends
id=6 Destructed
id=2 Destructed
id=1 Destructed

类成员的访问权限

通过关键字,可以定义类成员在什么位置允许访问。关键字分为三种:

  • private:私有成员,只能在成员函数内被访问到;缺省为私有成员
  • public:公有成员,在任意位置可以被访问
  • protected:保护成员,在被继承时使用

根据当前位置的不同,访问到类的成员也不同

  • 在类成员函数的内部:当前对象的全部属性、函数;同类其他对象的全部属性、函数
  • 在类的成员函数外部:只能访问该对象的公有成员
class CEmployee{
private:
char szName[30];
public:
int salary;
void setName(char *name);
void getName(char *name);
void averageSalary(CEmployee e1, CEmployee e2)
}

void CEmployee::setName(char *name){
strcpy(szName, name);
}
void CEmployee::getName(char *name){
strcpy(name, szName);
}
void CEmployee::averageSalary(CEmployee e1,CEmployee e2){
salary = (e1.salary + e2.salary )/2;
}

int main(){
CEmployee e;
strcpy(e.szName, "Tom1234567889"); // 编译错误, 不能访问私有成员
e.setName( "Tom"); // 编译正确
e.salary = 5000; // 编译正确
return 0;
}

设置私有成员的目的是,强制对成员变量的访问一定要通过成员函数进行,可以将私有成员隐藏起来。


第一周作业

测验

  1. 以下说法正确的是:
  • [ ] 每个对象内部都有成员函数的实现代码
  • [ ] 类的成员函数之间可以互相调用
  • [X] 一个类的私有成员函数内部不能访问本类的私有成员变量
  • [ ] 编写一个类时,至少要写一个成员函数
  1. 以下对类A的定义,哪个是正确的?
  • [x] A
class A {
int v;
public:
void Func();
};
A::void Func() { }
  • [ ] B // 缺少“;”
class A {
private:
int v;
public :
void Func() { }
}
  • [ ] C
class A {
int v;
public:
A next;
void Func() { }
};
  • [ ] D
class A {
int v;
A *next;
void Func() { }
};
  1. 假设有以下类A:
class A {
public:
int func(int a) { return a * a; }
};

以下程序片段,哪个是不正确的?

  • [ ] A a; A & r = a; r.func(5);
  • [ ] A *p = new A; p->func(5);
  • [ ] A a; a.func(5);
  • [x] A a, b; if( a!= b ) a.func(5);
  1. 以下程序,哪个是不正确的?
  • [ ] A
int main() {
class A {
public:
int v;
};
A * p = new A;
p->v = 4;
delete p;
return 0;
}
  • [ ] B // 缺少“;”
int main() {
class A {
public:
int v;
A * p;
};
A a;
a.p = new A;
delete a.p;
return 0;
}
  • [x] C
int main() {
class A {
public:
int v;
A * p;
};
A a;
a.p = & a;
return 0;
}
  • [ ] D
int main(){
class A { int v; };
A a;
a.v = 3;
return 0;
}
  1. 以下说法中正确的是:
  • [ ] 构造函数的返回值类型是 void
  • [ ] 一个类只能定义一个构造函数,但可以定义多个析构函数
  • [x] 一个类只能定义一个析构函数,但可以定义多个构造函数
  • [ ] 一个类一定会有无参构造函数构造函数的返回值类型是 void
  1. 对于通过 new 运算符生成的对象
  • [ ] 在程序结束时自动析构
  • [x] 在执行 delete 操作时会析构,如果没有执行delete操作,则在程序结束时自动析构
  • [ ] 执行 delete 操作时才能析构
  • [ ] 在包含该 new 语句的函数返回时自动析构

编程

描述

在一个学生信息处理程序中,要求实现一个代表学生的类,并且所有成员变量都应该是私有的。

输入

姓名,年龄,学号,第一学年平均成绩,第二学年平均成绩,第三学年平均成绩,第四学年平均成绩。

其中姓名、学号为字符串,不含空格和逗号;年龄为正整数;成绩为非负整数。

各部分内容之间均用单个英文逗号","隔开,无多余空格。

输出

一行,按顺序输出:姓名,年龄,学号,四年平均成绩(向下取整)。

各部分内容之间均用单个英文逗号","隔开,无多余空格。

样例

输入:

Tom,18,7817,80,80,90,70

输出:

Tom,18,7817,80

答案

#include <iostream>
#include <cstdio>
#include <string>

using namespace std;

class Student{
private:
char name[100], number[100];
int age, grade1, grade2, grade3, grade4;
public:
Student(char pname[], int page, char pnumber[], int pgrade1, int pgrade2, int pgrade3, int pgrade4){
strcpy(name, pname);
age = page;
strcpy(number, pnumber);
grade1 = pgrade1;
grade2 = pgrade2;
grade3 = pgrade3;
grade4 = pgrade4;
}
int getAverageGrade(){
return ((grade1 + grade2 + grade3 + grade4) / 4);
}

char * getName(){
return name;
}
char * getNum(){
return number;
}
int getAge(){
return age;
}

};
int main(int argc, char* argv[]){
char name[100], number[100];
int age, grade1, grade2, grade3, grade4;
cin.getline(name, 100, ',');
cin >> age;
char a = getchar();
cin.getline(number, 100, ',');

cin >> grade1 >> a >> grade2 >> a >> grade3 >> a >> grade4;
Student s(name, age, number, grade1, grade2, grade3, grade4);
cout << s.getName() << "," << s.getAge() << "," << s.getNum() << "," << s.getAverageGrade();
return 0;
}

输出:

Tom,22,2017110,99,98,89,95
Tom,22,2017110,95
Author: NYY
Link: http://yoursite.com/2018/08/06/c++/c-1-2/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.