1、class和struct的区别
- class默认成员访问权限是private,而struct中是public;
- class默认继承方式是private,而struct默认是public;
- class中有函数,struct中一般没有成员函数。
2、模板
(1)类模板
类模板同意用户为类定义一种模式。使得类中的某些数据成员,默认成员函数的参数,某些成员函数的返回值,能够取任意类型。它的存在不是代表一个详细的类,而是代表一类类。
template.h
template <class T>
class CTest {
private:
T num;
public:
CTest();
~CTest();
void Add(T a, T b);
T Print();
};
template.cpp
在类定义体外定义成员函数时,需在函数体外进行模板声明,且在类名后面加上<T>。
#include "template.h"
template <class T>
CTest<T>::CTest()
{
num = 0;
}
template <class T>
CTest<T>::~CTest()
{
num = 0;
}
template <class T>
void CTest<T>::Add(T a, T b)
{
num = a + b;
}
template <class T>
T CTest<T>::Print()
{
printf("num:%d\n", num);
return num;
}
(2)模板类
将类模板实例化成一个详细的类,即为模板类。
#include "template.cpp"
int main()
{
CTest<int> CTest1;
CTest1.Add(5, 6);
CTest1.Print();
return 1;
}
(3)函数模板
能够用来创建一个通用的函数,以支持多种不同的形参。避免重载函数的函数体反复设计。
#include <stdio.h>
template <class T>
T Decode(T a, T b)
{
T res = a * 100 + b;
return res;
}
int main()
{
double res = Decode<double>(5.4, 4.8);
printf("res:%f\n", res);
return 1;
}
(5)函数模板
将函数模板的类型形参实例化的过程。
3、重载
重载是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但它们的形式参数(指参数的个数、类型或者顺序)不同(只有返回值不同不可以重载)。
(1)函数重载
overload.h
#include <string>
using namespace std;
class CTest {
private:
int num;
public:
CTest();
~CTest();
void Print(int a);
void Print(double a);
void Print(string a);
};
overload.cpp
#include "overload.h"
CTest::CTest()
{
num = 0;
}
CTest::~CTest()
{
num = 0;
}
void CTest::Print(int a)
{
printf("a:%d\n", a * 100);
}
void CTest::Print(double a)
{
printf("a:%f\n", a * 1000);
}
void CTest::Print(string a)
{
printf("a:%s\n", a);
}
main.cpp
#include <stdio.h>
#include "overload.h"
int main()
{
double res = 5.4;
CTest test2;
test2.Print(res);
return 1;
}
(2)运算符重载
重载的运算符是带有特殊名称的函数。函数名是由关键字operator和其后要重载的运算符符号构成的。
overload.h
class CTest2 {
private:
int length;
public:
CTest2(int b);
~CTest2();
int operator+(CTest2 b);
};
overload.cpp
CTest2::CTest2(int b)
{
length = b;
}
CTest2::~CTest2()
{
}
int CTest2::operator+(CTest2 b)
{
int res = b.length + this->length;
return res;
}
main.cpp
#include <stdio.h>
#include "overload.h"
int main()
{
CTest2 test1(3);
CTest2 test2(5);
int res = test1 + test2;
return 1;
}
4、友元
在C++中,我们使用类对数据进行了隐藏和封装,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。
但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。
友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销);但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend。
友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。
(1)友元函数
friend.h
class CFriend{
private:
char name[20];
public:
CFriend(char * name);
~CFriend();
friend void showName(CFriend pFriend);
};
friend.cpp
#include "friend.h"
#include <iostream>
CFriend::CFriend(char* name)
{
strcpy(this->name,name);
}
CFriend::~CFriend()
{
}
void showName(CFriend pFriend)
{
//std::count<<pFriend.name<<std::endl;
printf("name:%s\n",pFriend.name);
}
main.cpp
#include <stdio.h>
#include <string.h>
#include "friend.h"
int main()
{
CFriend pfriend1("lihanyi");
showName(pfriend1);
return 1;
}
(2)友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。在类定义中声明友元类的写法如下:
friend.h
class CFriendA{
private:
char name[20];
public:
CFriendA(char * name);
~CFriendA();
friend class CFriendB;//B类可以访问A中的私有成员
friend int printAge();
};
class CFriendB{
private:
char name[20];
CFriendA *pa;
public:
CFriendB();
~CFriendB();
void showName();
};
friend.cpp
#include "friend.h"
#include <iostream>
CFriendA::CFriendA(char* name)
{
strcpy(this->name,name);
}
CFriendA::~CFriendA()
{
}
int printAge()
{
printf("age:%d\n",18);
return 1;
}
CFriendB::CFriendB()
{
strcpy(this->name,"zhaoyuzhen");
pa = new CFriendA("lihanyi");
}
CFriendB::~CFriendB()
{
}
void CFriendB::showName()
{
printf("name:%s\n",this->name);
printf("name:%s\n",pa->name);
}
main.cpp
#include <stdio.h>
#include <string.h>
#include "friend.h"
int main()
{
//CFriendA pfriend1("lihanyi");
CFriendB pfriend2;
//showName(pfriend1);
pfriend2.showName();
printAge();
return 1;
}
5、数组指针和指针数组
- 数组指针:是指针,括号括起指针名和星号,即指向一个数组的指针;
- 指针数组:是数组,指针名不带括号,数组中的每个值都是一个指针;
#include <iostream>
using namespace std;
int main()
{
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
int(*p1)[4];
p1 = (int(*)[4])a; // 数组指针
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << p1[i][j] << ' ';
}
}
cout << endl;
int* p2[4]; // 指针数组
for (int i = 0; i < 3; i++)
{
p2[i] = &a[i][0];
}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
cout << p2[i][j] << ' ';
}
}
cout << endl;
return 1;
}
6、extern关键字的作用
(1)当它与“C”一起连用的时候,如:extern "C" void fun(int a,int b);则告诉编译器在编译fun这个函数时候按着C的规矩去翻译,而不是C++的。(这与C++的重载有关,C++语言支持函数重载,C语言不支持函数重载;函数被C++编译器编译后在库中的名字与C语言的不同);
(2)当extern不与“C”在一起修饰变量或函数时,如:extern int g_Int;它的作用:声明函数或全局变量的作用范围的关键字。
7、static关键字的作用
(1)static加在局部变量前称为-》静态局部变量
普通局部变量存储于进程栈空间,使用完毕会立即释放。
静态局部变量(使用static修饰符定义的):
- 即使在声明时未赋初值,编译器也会把它初始化为0。
- 存储于进程的全局数据区,即使函数返回,它的值也会保持不变。
- 作用域为局部作用域。
Demo:
#include <stdio.h>
void fn(void)
{
int n = 10;
printf("n=%d\n", n);
n++;
printf("n++=%d\n", n);
}
void fn_static(void)
{
static int n = 10;
printf("static n=%d\n", n);
n++;
printf("n++=%d\n", n);
}
int main(void)
{
fn();
printf("--------------------\n");
fn_static();
printf("--------------------\n");
fn();
printf("--------------------\n");
fn_static();
return 0;
}
运行结果:
-> % ./a.out
n=10
n++=11
--------------------
static n=10
n++=11
--------------------
n=10
n++=11
--------------------
static n=11
n++=12
(2)static加在全局变量前称为-》静态全局变量
局变量本来就存储在静态区,因此static并不能改变其存储位置。
普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。
静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。
(3)static加在函数前-》静态函数
- 静态函数只能在声明它的文件中可见,其他文件不能引用该函数。非静态函数可以在另一个文件中直接引用,甚至不必使用extern声明。
- 不同的文件可以使用相同名字的静态函数,互不影响
(4)面向对象-》静态数据成员
- 存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中定义
- 静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝。
- 由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作。
- 和普通数据成员一样,静态数据成员也遵从public, protected, private访问规则
- 静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员名>=<值>
- 类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
同全局变量相比,使用静态数据成员有两个优势:
- 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性
- 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能
(4)面向对象-》静态成员函数
与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象,其特性如下:
- 静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数
- 出现在类体外的函数定义不能指定关键字static
- 非静态成员函数可以任意地访问静态成员函数和静态数据成员
8、const的作用
const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。
(1)const修饰普通类型的变量
a 被定义为一个常量,并且可以将 a 赋值给 b,但是不能给 a 再次赋值。
const int a = 7;
int b = a; // 正确
a = 8; // 错误,不能改变
(2)const修饰指针变量
- A:const修饰指针指向的内容,则内容为不可变量。
- B:const 修饰指针,则指针为不可变量。
- C:const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
案例A:
const int *p = 8;
案例B:
int a = 8;
int* const p = &a;
*p = 9; // 正确
int b = 7;
p = &b; // 错误
案例C:
int a = 8;
const int * const p = &a;
总结:"左定值,右定向,const修饰不变量"。
(3)const 修饰传参
- A:值传递的 const 修饰传递,一般这种情况不需要 const 修饰,因为函数会自动产生临时变量复制实参值。
- B:当 const 参数为指针时,可以防止指针被意外篡改。
- 自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。
void Cpf(int *const a)
{
cout<<*a<<" ";
*a = 9;
}
int main(void)
{
int a = 8;
Cpf(&a);
cout<<a; // a 为 9
system("pause");
return 0;
}
案例C:
#include<iostream>
using namespace std;
class Test
{
public:
Test(){}
Test(int _m):_cm(_m){}
int get_cm()const
{
return _cm;
}
private:
int _cm;
};
void Cmf(const Test& _tt)
{
cout<<_tt.get_cm();
}
int main(void)
{
Test t(8);
Cmf(t);
system("pause");
return 0;
}
(4)const修饰函数的返回值
- A: const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
- B: const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
- C: const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
(5)const修饰类成员函数
const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。
如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用 mutable 关键字修饰这个成员,mutable 的意思也是易变的,容易改变的意思,被 mutable 关键字修饰的成员可以处于不断变化中。
#include<iostream>
using namespace std;
class Test
{
public:
Test(int _m,int _t):_cm(_m),_ct(_t){}
void Kf()const
{
++_cm; // 错误
++_ct; // 正确
}
private:
int _cm;
mutable int _ct;
};
int main(void)
{
Test t(8,7);
return 0;
}
9、指针和引用的区别
(1)定义的区别
- 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
int a=1;int *p=&a;
int a=1;int &b=a;
- 引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。
- 可以有const指针,但是没有const引用;
- 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的);
- 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。
- ”sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小;
(2)作为传参
- 将指针作为参数进行传递时,事实上也是值传递,只不过传递的是地址。当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,
- 在将引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。
10、new和malloc的区别
(1)属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
(2)参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
(3)返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
(4)分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
(5)自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
(6)重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
(7)内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。
自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
11、多态、虚函数,纯虚函数
C++的多态性具体体现在运行和编译两个方面:
- 运行中的多态:通过继承和虚函数体现;
- 编译中的多态:体现在函数和运算符的重载上。
虚函数:virtual开头,允许子类对基类的虚函数重新定义。
纯虚函数:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。
抽象类:类型至少有一个纯虚函数。抽象类必须用作其他类的基类,而不能用于直接创建对象实例。
1、父类的析构函数需为虚函数。
如果不为虚函数,则基类指针指向子类对象时,只调用父类的析构函数,不调用子类的析构函数;
父类指针* pFather;
pFather = new CSon(); //父类构造,子类构造
...
delete pFather ;//子类析构,父类析构
2、父类函数为虚函数,子类才可进行重写;父类函数不为虚函数,子类不可进行重写
12、变量的声明和定义有什么区别
为变量分配地址和存储空间的称为定义,不分配地址的称为声明。
一个变量可以在多个地方声明,但是只在一个地方定义。