简述
C++是一种支持面向对象编程的编程语言。在C++中,类和对象是面向对象编程的核心概念。本文将详细介绍C++类和对象的所有内容,包括定义类、创建对象、成员函数、访问控制、构造函数、析构函数等,同时提供代码示例和注意事项。
类与对象
定义类
在C++中,类是一种用户自定义的数据类型。类定义了一个对象的属性和行为,是实现面向对象编程的基本单元。类定义的一般形式如下:
class 类名
{
private:
// 私有成员变量和函数
public:
// 公有成员变量和函数
protected:
// 保护成员变量和函数
};
在C++中,类的访问控制符有三种,分别是:public、protected和private。这些访问控制符定义了在类的外部如何访问类的成员变量和成员函数。
- public:public关键字用于指定公共成员。公共成员可以在类的内部和外部进行访问。这意味着,任何一个函数都可以访问公共成员,而不需要进行任何特殊的访问限制。
- protected:protected关键字用于指定保护成员。保护成员可以在类的内部和派生类中进行访问,但不能在类的外部进行访问。这意味着,只有在派生类中,才能访问保护成员,但在其他任何地方都无法访问。
- private:private关键字用于指定私有成员。私有成员只能在类的内部进行访问。这意味着,除了类本身以外,没有任何其他函数或类可以访问私有成员。
注意事项
访问控制符只对类的外部进行限制,而对类的内部则没有任何限制。这意味着,类的成员函数可以访问类的任何成员变量和成员函数,而不需要进行任何特殊的访问限制。
公共成员的例子:
class Person
{
public:
std::string name; // 公共成员变量
void setName(std::string name)
{
this->name = name;
} // 公共成员函数
};
int main()
{
Person p;
p.setName("Tom"); // 可以直接访问公共成员
std::cout << p.name << std::endl; // 可以直接访问公共成员
return 0;
}
在这个例子中,Person类的name成员变量和setName成员函数都是公共成员。因此,在main函数中,我们可以直接访问p对象的name成员变量和setName成员函数,而不需要进行任何特殊的访问限制。
保护成员的例子:
class Person
{
protected:
int age; // 保护成员变量
void setAge(int age)
{
this->age = age;
} // 保护成员函数
};
class Student : public Person
{
public:
void setStudentAge(int age)
{
setAge(age); // 可以访问基类的保护成员函数
}
};
int main() {
Student s;
s.setStudentAge(20); // 可以访问基类的保护成员函数
return 0;
}
在这个例子中,Person类的age成员变量和setAge成员函数都是保护成员,它们只能在类内部和派生类中进行访问。在Student类中,我们通过公共成员函数setStudentAge间接地访问了基类Person的保护成员函数setAge,从而实现了对age成员变量的设置。
私有成员的例子:
class Person
{
private:
std::string name; // 私有成员变量
public:
void setName(std::string name)
{
this->name = name;
} // 公共成员函数
std::string getName()
{
return name;
} // 公共成员函数
};
int main() {
Person p;
p.setName("Tom"); // 可以通过公共成员函数访问私有成员
std::cout << p.getName() << std::endl; // 可以通过公共成员函数访问私有成员
std::cout << p.name << std::endl; // 无法直接访问私有成员
return 0;
}
在这个例子中,Person类的name成员变量是私有成员,它只能在类的内部进行访问。在main函数中,我们通过公共成员函数setName和getName来间接地访问name成员变量,而无法直接访问它。
创建对象
在C++中,对象是类的实例。通过创建对象,可以访问类的成员变量和成员函数。创建对象的一般形式如下:
//类名 对象名;例如,使用上述Person类创建一个对象的代码如下:
Person p;
在创建对象之后,可以通过对象名访问类的成员变量和成员函数。例如,使用setName和setAge函数设置私有成员变量的值,使用print函数打印私有成员变量的值的代码如下:
Person p;
p.setName("Tom");
// 输出内容Tom
成员函数
在C++中,类的成员函数是定义在类内部的函数,用于实现类的行为。成员函数可以访问类的成员变量和成员函数,以及传递参数和返回值。
class Person
{
private:
std::string name; // 私有成员变量
public:
void setName(std::string name) // 成员函数
{
this->name = name;
} // 公共成员函数
std::string getName()
{
return name;
} // 公共成员函数
};
从上面可知:子类要访问父类中的保护变量、私有变量需要通过公共的成员函数访问,如果公共的成员函数中没有保护·私有变量的内容则不能访问。
析构函数
在C++中,析构函数是一种特殊的成员函数,用于销毁对象时清理资源。析构函数的名称与类名相同,但前面加上一个波浪号~,没有参数,也不需要显式调用。
~类名() {
// 析构函数体
}
下面的Person类添加一个析构函数,用于清理对象的代码如下:
class Person
{
private:
string name;
int age;
public:
Person(string n, int a)
{
name = n;
age = a;
}
~Person()
{
cout << "Destroying object " << name << endl;
}
void setName(string n)
{
name = n;
}
void setAge(int a)
{
age = a;
}
void print()
{
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
}
};
该析构函数在销毁对象时输出一条消息。例如,销毁上述Person对象的代码如下:
Person p("Tom", 20);
p.print();
// 销毁对象
输出结果为:
Name: Tom
Age: 20
Destroying object Tom
构造函数
在C++中,构造函数是一种特殊的成员函数,用于初始化对象的成员变量。构造函数的名称与类名相同,没有返回值类型,也不需要显式调用。构造函数的一般形式如下:
类名(参数列表)
{
// 构造函数体
}
例如,为上述Person类添加一个构造函数,用于初始化name和age成员变量的代码如下:
class Person
{
private:
string name;
int age;
public:
Person(string n, int a)
{
name = n;
age = a;
}
void setName(string n)
{
name = n;
}
void setAge(int a)
{
age = a;
}
void print()
{
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
}
};
该构造函数接受两个参数n和a,用于初始化name和age成员变量。例如,使用该构造函数创建一个Person对象的代码如下:
Person p("Tom", 20);
p.print();
输出结果为:
Name: Tom
Age: 20
静态成员
在C++中,静态成员是类的成员,但不属于任何对象。静态成员可以被类的所有对象共享,也可以通过类名直接访问。静态成员可以是静态数据成员或静态成员函数。静态数据成员是指属于类的变量,静态成员函数是指属于类的函数。静态数据成员的声明和定义必须在类的外部进行,并且要在前面加上关键字static。例如,为上述Person类添加一个静态数据成员count,用于记录创建的Person对象数量的代码如下:
class Person
{
private:
string name;
int age;
static int count; // 静态数据成员
public:
Person(string n, int a)
{
name = n;
age = a;
count++; // 每次创建对象时增加对象数量
}
~Person()
{
count--; // 每次销毁对象时减少对象数量
}
void setName(string n)
{
name = n;
}
void setAge(int a)
{
age = a;
}
void print()
{
cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
}
static int getCount() // 静态成员函数
{
return count;
}
};
int Person::count = 0; // 静态数据成员的定义
例如,使用该类创建三个对象并输出对象数量的代码如下:
Person p1("Tom",20);
Person p2("Jerry", 18);
Person p3("Mary", 22);
cout << "Object count: " << Person::getCount() << endl;
输出结果
Object count: 3
注意事项
- 类的定义必须在使用之前。
- 类的成员变量和成员函数的访问权限必须正确定义,遵循信息隐藏原则。
- 类的成员函数可以直接访问类的成员变量和静态成员,但不能直接访问非静态成员函数。
- 构造函数和析构函数是必须的,用于初始化和清理对象。
- 在创建对象时,如果没有显式地调用构造函数,将调用默认构造函数。
- 如果不需要复制对象,可以将复制构造函数和赋值运算符函数声明为私有,并不实现它们。
- 在使用静态成员时,可以通过类名直接访问,也可以通过对象访问。但访问权限必须正确设置。
class与struct的区别
与class非常相似的就是C语言中的struct了。
struct
struct是一种自定义数据类型,可以包含数据成员和函数成员。它的默认访问权限是public,默认的继承方式是public,默认成员类型是public。
struct Point {
int x;
int y;
void print() {
cout << "(" << x << ", " << y << ")" << endl;
}
};
上面的代码定义了一个Point结构体,它包含了两个int类型的数据成员x和y,以及一个打印函数print。由于默认的访问权限是public,因此这些成员都可以在结构体外部访问。可以使用如下方式初始化Point结构体:
Point p1 = {1, 2}; // 使用花括号初始化
Point p2 {3, 4}; // 不使用等号初始化
class
class也是一种自定义数据类型,可以包含数据成员和函数成员。它的默认访问权限是private,默认的继承方式是private,默认成员类型是private。下面是一个class的例子:
class Rectangle {
private:
int width;
int height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
int area() {
return width * height;
}
};
上面的代码定义了一个Rectangle类,它包含了两个int类型的私有数据成员width和height,以及一个公有构造函数Rectangle和一个公有成员函数area。由于默认的访问权限是private,因此这些成员只能在类内部或者通过类的公有成员函数访问。可以使用如下方式初始化Rectangle类:
Rectangle r1(2, 3); // 使用括号初始化
Rectangle r2 = Rectangle(4, 5); // 使用等号初始化
区别
- struct的默认访问权限是public,而class的默认访问权限是private。
- struct的默认继承方式是public,而class的默认继承方式是private。
- struct的默认成员类型是public,而class的默认成员类型是private。
- 在使用struct时可以省略关键字struct,而在使用class时不能省略关键字class。
- 在C++中,struct和class可以互相继承,区别仅在于默认访问权限和默认继承方式的不同。
对象的内存存储模型
相较于C语言,C++语言并没有额外增加内存消耗(确切说,在没有虚函数情况下)。 对于一个C++类对象,每个对象有独立的数据成员(非static),但是内存中成员函数只有一份,该类的所有对象共享成员函数。static数据成员属于类,该类的所有对象共享static数据成员,static数据成员存储在 静态存储区。 对象数据成员依据创建对象的方式不同,可能存在于栈上或者堆上。 成员函数存储在代码段。
无继承状态
Class Base
{
public:
int a;
int b;
virtual void function();
};
-
首先是虚函数表指针,该指针是由编译器 定义和初始化(编译阶段,编译器在构造函数内增加代码实现)
-
成员函数代码存储在 代码段,堆上构造虚函数表,将虚成员函数的地址存储在虚函数内。
-
数据成员按照声明的顺序布局;
单继承无虚函数
class A
{
public:
int a;
int b;
void fun1();
void fun2();
};
class B : public A
{
public:
int c;
void fun2();
}
- B继承A类其中a,b的数据是共享的,数据存储与栈区
- fun()1,A::fun2(),B::fun2() 都存在与代码区中
虚函数多继承
class A
{
public:
int a;
int b;
virtual void vfun1();
virtual void vfun2();
void fun1();
void fun2();
};
class B : public class A
{
public:
int c;
virtual void vfun1();
virtual void vfun2();
void fun2();
};
class C : public Class B
{
public:
int d;
virtual void vfun1();
void fun2();
}
- A类中存在着整型变量a,b,普通成员函数fun1(),fun2()。虚函数vfun1(),vfun2().由于虚函数的存在那么存在着虚表指针vpter* 指向A的虚函数表,A的虚函数表中存储着A::vfun1()与A::vfun2()虚函数在代码区中的地址。
- B继承了A的公共区内容,且B类中定义了一个整型变量c,同时对A类中的虚函数vfun1()虚函数进行重写,而虚函数vfun2()没有进行重写。所以,B类在内存的数据结构是a,b,与类A进行共享,C则属于B类自己的内容。A类无法访问。由于B重写了从A类中继承过来的虚函数vfun1(),而vfun2没有。所以B::vfun1()在代码区中是独立的地址空间,而vfun2()是共享A类的。
- C继承了B类的内容,所以C类中的数据a,b与A,B类共享,C的数据与B共享,d的数据属于C类自己。同理C类也没有重写A类中的vfun2()虚函数,所以在地址上vfun2()与A、B数据进行共享。vfun1()重写了所以属于自己的地址内存空间。
- 而对于非虚函数的成员B,C类是继承A类的fun1(),fun2()的,所以他们俩是共享A::fun1(),与A::fun2()的地址空间,而B类里面的非虚函数fun2()只是与A类的fun2()名字相同,实际上是独属于B::的fun2(),所以B类里面的fun2()在代码区中有自己的地址空间B::fun2()
- 同理对C类也是这样.C类里面fun2()是名字于A类相同而已,实际上存储的内存空间为C::fun2();
创建对象
class Student{
public:
//成员变量
char *name;
int age;
float score;
//成员函数
void say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
};
有了 Student 类后,就可以通过它来创建对象了,例如:
Student liLei; //创建对象
在创建对象时,class 关键字可要可不要,但是出于习惯我们通常会省略掉 class 关键字,例如:
class Student LiLei; //正确
Student LiLei; //同样正确
除了创建单个对象,还可以创建对象数组
Student allStu[100];
C语言中经典的指针在 C++ 中仍然广泛使用,尤其是指向对象的指针,上面代码中创建的对象 stu 在栈上分配内存,需要使用&获取它的地址,例如:
Student stu;
Student *pStu = &stu;
当然,你也可以在堆上创建对象,这个时候就需要使用前面讲到的new关键字
Student *pStu = new Student;
在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。但是通过 new 创建出来的对象就不一样了,它在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;堆内存由程序员管理,对象使用完毕后可以通过 delete 删除。在实际开发中,new 和 delete 往往成对出现,以保证及时删除不再使用的对象,防止无用内存堆积。
对象内的值初始化
在C++中,对象内的数据可以通过以下三种方式进行初始化:
1,默认初始化
如果一个类中的成员变量没有被显式地初始化,并且没有定义默认的构造函数,那么这个成员变量就会被默认初始化。默认初始化的规则是,基本数据类型会被初始化为0,而对象类型会被默认构造函数初始化。
class MyClass {
public:
int num; // num默认初始化为0
std::string str; // str默认初始化为空字符串
};
int main() {
MyClass obj;
std::cout << obj.num << std::endl; // 输出0
std::cout << obj.str << std::endl; // 输出空字符串
return 0;
}
2,成员初始化列表
使用成员初始化列表可以在对象创建时直接对成员变量进行初始化,而不是等到对象创建后再进行初始化。成员初始化列表在构造函数中使用冒号“:”进行声明,多个成员之间用逗号“,”隔开。
class MyClass {
public:
int num;
std::string str;
MyClass(int n, const std::string& s): num(n), str(s) {} // 使用成员初始化列表
};
int main() {
MyClass obj(100, "hello");
std::cout << obj.num << std::endl; // 输出100
std::cout << obj.str << std::endl; // 输出hello
return 0;
}
3,赋值初始化
对象的成员变量也可以通过赋值进行初始化,赋值初始化发生在对象创建后。。
class MyClass {
public:
int num;
std::string str;
};
int main() {
MyClass obj;
obj.num = 100;
obj.str = "hello";
std::cout << obj.num << std::endl; // 输出100
std::cout << obj.str << std::endl; // 输出hello
return 0;
}
总结
在C++中,对象内的数据可以通过默认初始化、成员初始化列表和赋值初始化进行初始化。默认初始化会根据数据类型进行默认值的初始化,成员初始化列表在对象创建时直接对成员变量进行初始化,赋值初始化发生在对象创建后。使用成员初始化列表可以提高代码的可读性和效率。
对象数组
在C++中,我们可以定义对象数组,也就是将同一种类型的对象存储在一个数组中。定义对象数组的语法如下:
ClassName arrayName[arraySize];
其中,ClassName是对象类型,arrayName是数组的名称,arraySize是数组的大小。例如,下面定义了一个包含三个MyClass对象的对象数组
class MyClass {
public:
int num;
MyClass(int n) : num(n) {}
};
int main() {
MyClass myArray[3] = {MyClass(1), MyClass(2), MyClass(3)};
return 0;
}
上述代码中,我们定义了一个包含三个MyClass对象的对象数组myArray。这个数组被初始化为三个MyClass对象,每个对象的num值分别为1、2、3。当我们定义对象数组时,每个对象都会被自动调用它们的构造函数进行初始化。因此,我们必须确保该类型有可用的构造函数。访问对象数组的元素与访问一般的数组元素一样,可以使用下标运算符“[]”来访问。
MyClass myArray[3] = {MyClass(1), MyClass(2), MyClass(3)};
std::cout << myArray[0].num << std::endl; // 输出1
std::cout << myArray[1].num << std::endl; // 输出2
std::cout << myArray[2].num << std::endl; // 输出3
总结
在C++中,我们可以定义对象数组来存储同一类型的对象。定义对象数组的语法为ClassName arrayName[arraySize]。每个对象都会被自动调用它们的构造函数进行初始化。我们可以使用下标运算符“[]”来访问对象数组的元素。
C++成员对象和封闭类
在C++中,成员对象是一个类的成员变量,这个成员变量是另一个类的对象。这种情况下,我们通常把包含成员对象的类称为封闭类,被包含的成员对象称为嵌套类或成员子对象。例如,下面定义了一个封闭类Person,包含一个成员对象Birthday:
class Birthday {
public:
int year;
int month;
int day;
Birthday(int y, int m, int d) : year(y), month(m), day(d) {}
};
class Person {
public:
std::string name;
Birthday birthday; // Birthday作为成员对象
Person(const std::string& n, const Birthday& b) : name(n), birthday(b) {}
};
int main() {
Birthday b(1990, 10, 1);
Person p("Tom", b);
std::cout << p.name << "'s birthday is " << p.birthday.year << "-" << p.birthday.month << "-" << p.birthday.day << std::endl;
return 0;
}
上述代码中,我们定义了一个Birthday类和一个Person类,其中Person类包含一个Birthday对象作为成员对象。在main函数中,我们创建了一个Birthday对象b和一个Person对象p,将b作为参数传递给p的构造函数。在输出语句中,我们访问了p对象的name成员变量和birthday成员对象的year、month和day成员变量。
需要注意的是,成员对象的初始化顺序是在封闭类构造函数的初始化列表中按照声明顺序初始化的。例如,如果我们在Person类中将birthday成员对象定义在name成员变量之前,那么在Person的构造函数中,初始化列表中也应该将birthday放在name前面。
class Person {
public:
Birthday birthday;
std::string name;
Person(const Birthday& b, const std::string& n) : birthday(b), name(n) {}
};
总结
在C++中,成员对象是一个类的成员变量,这个成员变量是另一个类的对象。封闭类包含成员对象,被包含的成员对象称为嵌套类或成员子对象。成员对象的初始化顺序是在封闭类构造函数的初始化列表中按照声明顺序初始化的。
this指针
在C++中,每个对象都有一个指向自己的指针,这个指针称为this指针。this指针可以让我们在类的成员函数中访问调用该函数的对象的成员变量和成员函数。this指针是一个隐式参数,它被自动传递给成员函数。在类的成员函数中,可以使用this指针来访问对象的成员变量和成员函数。例如:
class Person {
public:
std::string name;
int age;
void setName(const std::string& n)
{
this->name = n; // 使用this指针访问name成员变量
}
void setAge(int a)
{
age = a; // 直接访问age成员变量
}
void printInfo()
{
std::cout << "Name: " << this->name << ", Age: " << this->age << std::endl; // 使用this指针访问成员变量
}
};
int main()
{
Person p;
p.setName("Tom");
p.setAge(20);
p.printInfo();
return 0;
}
上述代码中,我们定义了一个Person类,其中包含两个成员变量name和age,以及三个成员函数setName、setAge和printInfo。在setName和printInfo函数中,我们使用了this指针来访问对象的成员变量name和age。在setAge函数中,我们直接访问了age成员变量。在main函数中,我们创建了一个Person对象p,并分别调用了它的setName、setAge和printInfo函数来设置和输出对象的成员变量值。
需要注意的是,this指针在成员函数中是一个局部变量,它指向调用该函数的对象,因此每个对象都有自己的this指针。在成员函数中,我们可以使用this指针来访问调用该函数的对象的成员变量和成员函数。如果一个成员函数没有使用this指针访问任何成员变量或成员函数,那么this指针将不会被使用,编译器不会为该函数生成this指针。
this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。
总结
在C++中,每个对象都有一个指向自己的指针,这个指针称为this指针。this指针可以让我们在类的成员函数中访问调用该函数的对象的成员变量和成员函数。在成员函数中,我们可以使用this指针来访问调用该函数的对象的成员变量和成员函数。如果一个成员函数没有使用this指针访问任何成员变量或成员函数,那么this指针将不会被使用,编译器不会为该函数生成this指针。
C++静态成员变量
对象的内存中包含了成员变量,不同的对象占用不同的内存使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。例如有两个相同类型的对象 a、b,它们都有一个成员变量 m_name,那么修改 a.m_name 的值不会影响 b.m_name 的值。
有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数,以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1。
class Student
{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //静态成员变量
private:
char *m_name;
int m_age;
float m_score;
}
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为 m_total 分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了 m_total,也会影响到其他对象。static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
//通过类类访问 static 成员变量
Student::m_total = 10;
//通过对象来访问 static 成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通过对象指针来访问 static 成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
C++静态成员函数
在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
#include <iostream>
using namespace std;
class Student
{
public:
Student(char *name, int age, float score);
void show();
public: //声明静态成员函数
static int getTotal();
static float getPoints();
private:
static int m_total; //总人数
static float m_points; //总成绩
private:
char *m_name;
int m_age;
float m_score;
};
int Student::m_total = 0;
float Student::m_points = 0.0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score)
{
m_total++;
m_points += score;
}
void Student::show()
{
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义静态成员函数
int Student::getTotal()
{
return m_total;
}
float Student::getPoints()
{
return m_points;
}
int main()
{
(new Student("小明", 15, 90.6)) -> show();
(new Student("李磊", 16, 80.5)) -> show();
(new Student("张华", 16, 99.0)) -> show();
(new Student("王康", 14, 60.8)) -> show();
int total = Student::getTotal();
float points = Student::getPoints();
cout<<"当前共有"<<total<<"名学生,总成绩是"<<points<<",平均分是"<<points/total<<endl;
return 0;
}
小明的年龄是15,成绩是90.6
李磊的年龄是16,成绩是80.5
张华的年龄是16,成绩是99
王康的年龄是14,成绩是60.8
当前共有4名学生,总成绩是330.9,平均分是82.725
C++ const成员变量和成员函数
const 成员变量的用法和普通 const 变量的用法相似,只需要在声明时加上 const 关键字。初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表
class Student
{
public:
Student(char *name, int age, float score);
void show();
//声明常成员函数
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show()
{
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义常成员函数
char * Student::getname() const
{
return m_name;
}
int Student::getage() const
{
return m_age;
}
float Student::getscore() const
{
return m_score;
}
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。const 成员函数也称为常成员函数。
注意事项
需要强调的是,必须在成员函数的声明和定义处同时加上 const 关键字。char *getname() const和char *getname()是两个不同的函数原型,如果只在一个地方加 const 会导致声明和定义处的函数原型冲突。
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()
- 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const
C++ const对象
在 C++ 中,const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)了
#include <iostream>
using namespace std;
class Student
{
public:
Student(char *name, int age, float score);
public:
void show();
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show()
{
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
char * Student::getname() const
{
return m_name;
}
int Student::getage() const
{
return m_age;
}
float Student::getscore() const
{
return m_score;
}
int main()
{
const Student stu("小明", 15, 90.6);
//stu.show(); //error
cout<<stu.getname()<<"的年龄是"<<stu.getage()<<",成绩是"<<stu.getscore()<<endl;
const Student *pstu = new Student("李磊", 16, 80.5);
//pstu -> show(); //error
cout<<pstu->getname()<<"的年龄是"<<pstu->getage()<<",成绩是"<<pstu->getscore()<<endl;
return 0;
}
C++友元函数和友元类
在 C++ 中,一个类中可以有 public、protected、private 三种属性的成员,通过对象可以访问 public 成员,只有本类中的函数可以访问本类的 private 成员。现在,我们来介绍一种例外情况——友元(friend)。借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。
1,友元函数
在单继承中,友元函数的使用方式与普通类中的使用方式相同。可以在类中声明一个函数为友元函数,使得该函数可以访问该类的私有成员变量和成员函数。
class A {
private:
int x;
public:
friend void foo(A& a);
};
void foo(A& a) {
a.x = 10; // 可以访问A类的私有成员变量x
}
int main() {
A a;
foo(a);
return 0;
}
在上面的代码中,我们声明了一个类A,并在其中声明了一个函数foo
为友元函数。在foo
中,我们可以直接访问A类的私有成员变量x,并将其赋值为10。在主函数中,我们创建了一个A类的实例a
,并将其作为参数传递给foo
函数,从而实现了对A类的私有成员变量x的修改。
2,友元类
友元类的使用方式与普通类中的使用方式相同。可以在类中声明另外一个类为友元类,使得该类可以访问该类的私有成员变量和成员函数。下面是一个示例:
class A {
private:
int x;
public:
friend class B;
};
class B {
public:
void foo(A& a) {
a.x = 10; // 可以访问A类的私有成员变量x
}
};
int main() {
A a;
B b;
b.foo(a); // 调用B类的成员函数foo,修改A类的私有成员变量x
return 0;
}
在上面的代码中,我们声明了一个类A,并在其中声明了一个类B为友元类。在类B中,我们声明了一个成员函数foo
,并在其中访问了A类的私有成员变量x,并将其赋值为10。在主函数中,我们创建了一个A类的实例a
和一个B类的实例b
,并将a
作为参数传递给b
的成员函数foo
,从而实现了对A类的私有成员变量x的修改。
注意事项
需要注意的是,如果一个类A是另外一个类B的友元类,那么在B中可以访问A的私有成员变量和成员函数,但是在A中不能访问B的私有成员变量和成员函数。这是由于友元关系是单向的。如果需要在A中访问B的私有成员变量和成员函数,需要在B中再次声明A为友元类。
作用域
C++的作用域是指变量、函数、类等标识符在程序中可以被访问的范围。在C++中,作用域分为以下四种:
-
全局作用域:全局作用域是指在程序的任何地方都可以访问的标识符。在C++中,全局作用域包括全局变量、全局函数和命名空间中的标识符。
-
类作用域:类作用域是指在类的成员函数中可以访问的标识符。在C++中,类作用域包括类的成员变量、成员函数和友元函数。
-
块作用域:块作用域是指在一个代码块(花括号{}之间的代码)中定义的标识符。在C++中,块作用域包括局部变量、函数参数和代码块中的标识符。
-
命名空间作用域:命名空间作用域是指在命名空间中定义的标识符。在C++中,命名空间作用域包括命名空间中的变量、函数和类。
下面通过代码是全局作用域
#include <iostream>
using namespace std;
int global_var = 10; // 全局变量
namespace A {
int namespace_var = 20; // 命名空间变量
class Test {
public:
void func() {
int class_var = 30; // 类的成员变量
cout << "class_var: " << class_var << endl;
cout << "namespace_var: " << namespace_var << endl;
cout << "global_var: " << global_var << endl;
}
};
}
int main() {
int local_var = 40; // 局部变量
A::Test t;
t.func();
cout << "local_var: " << local_var << endl;
cout << "namespace_var: " << A::namespace_var << endl;
cout << "global_var: " << global_var << endl;
return 0;
}
在上面的代码中,我们定义了一个全局变量global_var
,一个命名空间A
,一个命名空间变量namespace_var
和一个类Test
,在类中定义了一个成员变量class_var
和一个成员函数func
,在main
函数中定义了一个局部变量local_var
。在Test
类的成员函数func
中,可以访问类的成员变量class_var
、命名空间变量namespace_var
和全局变量global_var
。在main
函数中,可以访问局部变量local_var
、命名空间变量namespace_var
和全局变量global_var
。
类作用域中的类成员作用域
class Person {
public:
void setName(std::string name) {
this->name = name;
}
std::string getName() {
return name;
}
private:
std::string name;
};
int main() {
Person person;
person.setName("Alice");
std::cout << person.getName() << std::endl;
return 0;
}
类作用域
class Person {
public:
class Address {
public:
Address(std::string country, std::string city) : country(country), city(city) {}
std::string getCountry() {
return country;
}
std::string getCity() {
return city;
}
private:
std::string country;
std::string city;
};
void setAddress(Address address) {
this->address = address;
}
Address getAddress() {
return address;
}
private:
Address address;
};
int main() {
Person::Address address("China", "Beijing");
Person person;
person.setAddress(address);
std::cout << person.getAddress().getCountry() << ", " << person.getAddress().getCity() << std::endl;
return 0;
}
在上面的例子中,类Person定义了一个类类型Address,可以在类外部使用“Person::Address”来访问这个类类型。
块作用域
C++的块作用域是指在一对花括号内定义的变量或对象只在这个花括号内有效,出了这个花括号就失效了。这种特性可以用来控制变量的可见性和生命周期。
#include <iostream>
int main() {
int x = 1;
{
int y = 2;
std::cout << "x+y=" << x+y << std::endl; // 输出 x+y=3
}
std::cout << "x+y=" << x+y << std::endl; // 编译错误,y不在作用域内
return 0;
}
在上面的代码中,变量x
是在main
函数的作用域中定义的,它的作用域是整个main
函数。而变量y
是在{}
内部定义的,它的作用域只在这个花括号内部。所以在第一个cout
语句中,x
和y
都是有效的,可以正常计算。但在第二个cout
语句中,y
已经超出了作用域,所以编译器会报错。
需要注意的是,如果在内部作用域中定义了与外部作用域中同名的变量,那么内部作用域中的变量会覆盖外部作用域中的变量,直到内部作用域结束为止。例如:
#include <iostream>
int main() {
int x = 1;
{
int x = 2;
std::cout << "x=" << x << std::endl; // 输出 x=2
}
std::cout << "x=" << x << std::endl; // 输出 x=1
return 0;
}
在上面的代码中,内部作用域中定义了一个名为x
的变量,它会覆盖外部作用域中的x
变量。所以第一个cout
语句输出的是内部作用域中的x
变量,而第二个cout
语句输出的是外部作用域中的x
变量。
命名空间作用域
命名空间的作用域是指命名空间中定义的标识符(如函数、变量、类等)在程序中的可见范围。命名空间的作用域可以分为两种
1,未限定作用域
未限定作用域是指在命名空间外部直接使用命名空间中定义的标识符
namespace my_namespace {
int my_var = 1;
}
int main() {
std::cout << my_namespace::my_var << std::endl; // 输出1
return 0;
}
在未限定作用域中,必须使用命名空间限定符(如my_namespace::
)来访问命名空间中的标识符。
2. 限定作用域
限定作用域是指在命名空间内部或使用了命名空间限定符的作用域中使用命名空间中定义的标识符。例如:
namespace my_namespace {
int my_var = 1;
void my_func() {
int my_var = 2;
std::cout << my_var << std::endl; // 输出2
std::cout << my_namespace::my_var << std::endl; // 输出1
}
}
int main() {
my_namespace::my_func();
return 0;
}
在限定作用域中,可以直接使用命名空间中定义的标识符,也可以使用命名空间限定符来访问其他命名空间中的标识符。
需要注意的是,命名空间的作用域也受到同名标识符的影响。如果在命名空间中定义了与全局作用域或其他命名空间中的标识符同名的标识符,因此需要做出限制。如下
namespace my_namespace {
int my_var = 1;
}
int my_var = 2;
int main() {
std::cout << my_var << std::endl; // 输出2
std::cout << my_namespace::my_var << std::endl; // 输出1
return 0;
}
命名空间可以嵌套
namespace A {
int a = 1000;
namespace B {
int a = 2000;
}
}
void test03()
{
cout<<"A中的a = "<<A::a<<endl; //1000
cout<<"B中的a = "<<A::B::a<<endl; //2000
}
命名空间是开放的,即可以随时把新的成员加入已有的命名空间中(常用)
namespace A {
int a = 100;
int b = 200;
}
//将c添加到已有的命名空间A中
namespace A {
int c = 300;
}
void test04()
{
cout<<"A中a = "<<A::a<<endl;//100
cout<<"A中c = "<<A::c<<endl;//200
}
命名空间中的函数 可以在“命名空间”外 定义
namespace A {
int a=100;//变量
void func();
}
void A::func()//成员函数 在外部定义的时候 记得加作用域
{
//访问命名空间的数据不用加作用域
cout<<"func遍历a = "<<a<<endl;
}
void funb()//普通函数
{
cout<<"funb遍历a = "<<A::a<<endl;
}
void test06()
{
A::func();
funb();
}
无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接
namespace{
int a = 10;
void func(){
cout<<"hello namespace"<<endl;
}
}
void test(){
//只能在当前源文件直接访问a 或 func
cout<<"a = "<<a<<endl;
func();
}
命名空间可取别名
namespace veryLongName{
int a = 10;
void func(){ cout << "hello namespace" << endl; }
}
void test(){
namespace shortName = veryLongName;
cout << "veryLongName::a : " << shortName::a << endl;
veryLongName::func();
shortName::func();
}
C++ using
C++中的using是一个关键字,它有两种不同的用法。
1. using声明
using声明用来引入一个命名空间中的名称,使其在当前作用域中可见。它的语法格式为:
using namespace_name::name;
其中,namespace_name是命名空间的名称,name是命名空间中的某个名称,比如一个函数、变量或类型。
例如,我们有一个命名空间my_namespace,其中定义了一个函数my_function:
namespace my_namespace {
void my_function() {
// ...
}
}
如果我们想在另一个命名空间中使用这个函数,可以这样写:
using my_namespace::my_function;
int main() {
my_function();
return 0;
}
这样,在main函数中就可以直接使用my_function了。
需要注意的是,using声明具有传递性,也就是说,如果在一个命名空间中使用using声明引入了另一个命名空间中的名称,那么在该命名空间的子命名空间中也可以直接使用该名称。例如:
namespace my_namespace {
void my_function() {
// ...
}
}
namespace my_subnamespace {
using my_namespace::my_function;
}
int main() {
my_subnamespace::my_function();
return 0;
}
在my_subnamespace中也可以直接使用my_function。
需要注意的是,using声明有可能会导致命名冲突,因此应该尽量避免在全局作用域中使用using声明。
2. using别名
using别名用来为一个类型定义一个别名,使其更方便使用。它的语法格式为:
using new_name = old_name;
其中,new_name是新的名称,old_name是被定义的类型。
例如,我们想为一个长长的类型名字std::vector<std::pair<int, std::string>>定义一个别名,可以这样写:
using my_vector = std::vector<std::pair<int, std::string>>;
这样,我们就可以使用my_vector来代替std::vector<std::pair<int, std::string>>了。
需要注意的是,using别名只是给类型起了一个新的名称,并不会创建一个新的类型。因此,在使用别名时,需要注意与原类型的兼容性问题。
综上所述,using在C++中有两种用法:using声明和using别名。using声明用来引入命名空间中的名称,using别名用来为类型定义一个别名。在使用时,需要注意命名冲突和兼容性问题。
C++ string(C++字符串)
C++中的string是一个类,用于存储和操作字符串。它的定义在头文件string中。
使用string的第一步是包含头文件string:
#include <string>
创建一个string对象的方法有很多种,可以使用字符串字面量、字符数组、其他string对象等等。
string str1 = "Hello World!"; // 使用字符串字面量初始化
string str2("I am a string."); // 使用字符数组初始化
string str3(str1); // 使用其他string对象初始化
string对象可以像普通字符串一样被访问和修改,使用下标运算符[]或者at()方法。
string str = "Hello World!";
cout << str[0] << endl; // 输出'H'
str[0] = 'J';
cout << str << endl; // 输出'Jello World!'
string对象可以使用加号运算符+进行拼接,也可以使用append()方法。
string str1 = "Hello ";
string str2 = "World!";
string str3 = str1 + str2; // 拼接字符串
cout << str3 << endl; // 输出'Hello World!'
str1.append(str2); // 使用append()方法拼接字符串
cout << str1 << endl; // 输出'Hello World!'
string对象的长度可以使用size()或者length()方法获取。
string str = "Hello World!";
cout << str.size() << endl; // 输出'12'
cout << str.length() << endl; // 输出'12'
string对象可以使用substr()方法获取子串,参数为起始位置和子串长度。
string str = "Hello World!";
string sub1 = str.substr(6); // 获取从第6个字符开始的子串
string sub2 = str.substr(0, 5); // 获取从第0个字符开始长度为5的子串
cout << sub1 << endl; // 输出'World!'
cout << sub2 << endl; // 输出'Hello'
string对象可以使用find()方法查找子串,返回子串第一次出现的位置,如果没有找到则返回string::npos。
string str = "Hello World!";
int pos = str.find("World");
if (pos != string::npos) {
cout << "Found at position " << pos << endl; // 输出'Found at position 6'
} else {
cout << "Not found." << endl;
}
C++ string的高级用法
1. 字符串的迭代器
C++中的字符串类型std::string支持迭代器,可以通过迭代器来访问字符串中的每个字符。常用的迭代器有begin()和end(),分别指向字符串的第一个字符和最后一个字符的下一个位置。例如:
std::string str = "hello world";
for (auto it = str.begin(); it != str.end(); ++it) {
std::cout << *it << " ";
}
输出结果为:
h e l l o w o r l d
2. 字符串的切割
C++中的字符串类型std::string没有内置的切割函数,但可以通过STL中的std::istringstream类来实现字符串的切割。例如:
std::string str = "hello world";
std::istringstream iss(str);
std::string token;
while (std::getline(iss, token, ' ')) {
std::cout << token << std::endl;
}
以上代码将字符串按照空格分割成多个子串,并依次输出每个子串。输出结果为:
hello
world
3. 字符串的替换
C++中的字符串类型std::string提供了replace()函数来实现字符串的替换。例如:
std::string str = "hello world";
str.replace(0, 5, "hi");
std::cout << str << std::endl;
以上代码将字符串中从位置0开始的5个字符替换成"hi",输出结果为:
hi world
4. 字符串的查找和替换
C++中的字符串类型std::string提供了find()函数和replace()函数来实现字符串的查找和替换。例如:
std::string str = "hello world";
std::string oldStr = "world";
std::string newStr = "universe";
size_t pos = str.find(oldStr);
if (pos != std::string::npos) {
str.replace(pos, oldStr.size(), newStr);
}
std::cout << str << std::endl;
以上代码将字符串中第一个出现的"world"替换成"universe",输出结果为:
hello universe
5. 字符串的格式化输出
C++中的字符串类型std::string提供了类似于printf()函数的格式化输出函数——sprintf(),可以将多个参数按照指定的格式输出成一个字符串。例如:
std::string str;
char buf[1024];
int n = 123;
double d = 3.14;
sprintf(buf, "n = %d, d = %.2f", n, d);
str = buf;
std::cout << str << std::endl;
以上代码将整数n和浮点数d按照指定的格式输出成一个字符串,并输出结果为:
n = 123, d = 3.14
6. 字符串的转换
C++中的字符串类型std::string提供了多个函数来实现字符串和其他类型的相互转换,例如:
- std::stoi():将字符串转换成整数。 - std::stof():将字符串转换成浮点数。 - std::to_string():将整数或浮点数转换成字符串。std::string str = "123";
int n = std::stoi(str);
float f = std::stof(str);
std::string str2 = std::to_string(n);
std::string str3 = std::to_string(f);
std::cout << n << ", " << f << ", " << str2 << ", " << str3 << std::endl;
以上代码将字符串"123"转换成整数和浮点数,并将整数和浮点数转换成字符串输出,输出结果为:
123, 123, 123, 123.000000