名称空间:
可能一个程序使用的两个组件,都含有名为wanda()的函数,此时需要用名称空间指出使用的是哪个版本的wanda()
例如有A和B两个组件都含有wanda()函数
using namespace A; wanda(); == A::wanda()
函数:
函数原型
为什么需要原型?
1.告诉编译器调用这个函数需要什么参数,没提供的话就报错
2.函数返回值会被放在指定的位置(CPU寄存器或者内存中),需要告诉编译器检索多少字节以及如何解释他们
编译器为什么不直接去找函数实现要这些信息?
1.效率不高
2.实现可能不在文件中,而在别的文件,编译器编译的时候可能无权访问那些文件。比如编译了a.c,但用到的实现在b.c
如果函数是位于库中,情况也是如此。
函数参数
注意事项
传递了一个参数,但类型不正确怎么办?
C语言:造成奇怪的错误。例如需要int但传了double,则函数将只检查64位的前32位,并试图将他们解释为一个int
C++: 自动将传递的值转换为指定的类型,条件是两者都是算术类型,像整型转为指针之类的无意义操作,C++也不会强制转换
如果参数只是拿来使用而不修改
使用const修饰
const int *p 指向的int值不能修改
int * const p p指向的地址不能修改
结构体作为参数:
直接传值:一般不直接拿结构体做参数,而是传它的地址,因为当结构体很大时复制结构体将增加内存要求,降低系统运行速度。
引用变量
引用变量是已有变量的一个别名,引用变量或者已有变量都可修改值。
引用变量的主要作用是作为函数的形参,通过将引用变量用作参数,函数将使用原始数据而不是副本。
这样除了指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。
创建引用变量
int rats;
int & rodents = rats; //创建rats的引用变量
rodents++; //应用变量的使用方式
引用和指针的区别
必须在声明引用时将其初始化,而不能像指针那样先声明再赋值。这也意味着一旦引用,无法修改引用的对象
int rats,test;
int & rodents = rats; //创建rats的引用变量
rodents = test; //效果并不是rodents变成了test的引用,而是将test的值赋给了rodents所引用的地址
引用作为参数
void swap1(int &a,int &b);
void swap2(int a,int b);
void swap1(int &a,int &b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
void swap2(int a,int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
int main()
{
int a=0,b=1;
swap1(a,b); //能成功交换
swap2(a,b); //交换失败
return 0;
}
可以看到,引用和直接传值的唯一差别只是函数申明,但效果却完全不同(成功交换和不成功)
引用作为返回值
为什么要返回引用
int & add(int &a,const int &b)
{
a+=b;
return a;
}
int c = add(6,7);
普通函数返回值需要先放到一个临时位置,再从这个临时位置赋给c。
返回引用的话讲直接从a赋值给c,效率更高。
返回引用注意事项
不能返回已经不存在的内存单元的引用,比如:
int & add(int &a,const int &b)
{
int c;
c = a+b;
return c; //错误
}
由于c是个局部变量,函数结束后就被销毁了,因此在函数外试图使用它的引用是错误的。
默认参数
假设有一个函数left,用于返回str的从左往右的前n个字符组成的字符串
char *left(const char *str,int n);
当用户经常只需要使用n=1的场景,偶尔才使用n>1的场景时,C++提供了默认参数这个新内容
char *left(const char *str,int n = 1); //只需要在声明处写默认值
char *left(const char *str,int n ) //实现不用写默认值
{
...
}
当不填写n时,默认n=1,填写了n时,使用填写的值
left("name") == left("name",1)
注意事项:
当有多个参数时,必须从右向左添加默认值,也就是说你想为某个参数设默认值,必须先给它右边的所有参数设默认值。
毕竟,像下面这样的函数调用还是不能接受的
func(1,,2);
内联函数(inline)
普通函数的调用:先保存程序当前地址 =》 跳转到函数地址执行代码 =》结束后返回原先的地址
内联函数的调用:编译时直接在调用处展开函数代码 =》顺序执行
内联函数优点:内联函数的运行速度比常规函数稍快(省去跳转的时间)
内联函数缺点:占用他更多内存,比如10个地方调用,就会有10个函数备份
适用场景:函数体小且经常被调用,调用开销>运行开销
注意事项: ① inline只是给编译器的一个建议,最终作不作为内联函数由编译器决定。
内联函数不能递归。内联函数不能有循环,switch(编译器认为是复杂函数不能作为内联)
② inline修饰的函数不支持跨文件,只在当前文件内有效。
即不建议声明和定义分离,分离会导致链接错误。一般写在头文件中。
inline被展开的时候,就没有函数地址了,链接就会出问题。
③ 是基于实现,不是基于声明
inline void Foo(int x,int y); //inline与函数声明放在一起,不能成为lnline
void Foo(int x,int y){}
void Foo(int x,int y);
inline void Foo(int x,int y){} //与函数的定义体放在一起,可以成为内联函数
内联函数和宏的区别
inline 在编译阶段进行参数 类型检查和安全检查,宏处理在预编译期间,不进行参数 类型检查和安全检查。
nline是一种更安全的宏
函数重载(函数多态)
函数重载指的是可以有多个同名的函数,通过函数的参数列表区分。
int func(int a); #1
int func(char *b); #2
func(1); //调动#1
func("nihao")//调用#2
一些看起来彼此不同的函数是不能共存的:
double cube(double x);
double cube(double &x);
虽然看起来两者不一样,但是在编译器角度看,随便传个double参数进来,和double double &都匹配,
因此函数就不知道该用哪个函数了,编译器将类型引用和类型视为相同。
同样的,编译器也不区分const和非const。
什么时候使用函数重载:
在函数基本上执行相同的任务,但使用不同的数据时。
类中的函数(接口,方法)
公共接口(public函数):
让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。
比如,要计算string类的对象包含多少字符,不需要打开对象,只需要使用string类提供的size()接口
类声明示例:
(通常把数据成员放私有部分,成员函数放公有部分,使用者通过成员函数修改或访问成员变量)
class Stock
{
private:
std::string company; //股票公司
long shares; //股票数
double share_val; //股价
double total_val; //股票价值
void set_tot() {total_val = shares*share_val;} //就地实现的方式
public:
void acquire(const std::string & co,long n,double pr); //单使用声明的方式
void buy(long num,double price);
void sell(long num,double price);
void update(double price);
void show();
};
实现类成员函数(比如上文的update函数)特点:
定义成员函数时,使用作用域解析运算符(::)来表示函数所属的类。
类成员函数可以访问类的private组件。
void Stock::update(double price)
{
share_val = price;
set_tot();
}
内联方法
其实现位于类声明中的函数将自动成为内联函数,因此Stock::set_tot()是一个内联函数。
类声明常将短小的成员函数作为内联函数,set_tot()符合这样的条件。
也可以在类声明之外定义成员函数,并使其成为内联函数:
inline void Stock::update(double price)
{
share_val = price;
set_tot();
}
//使用类的成员函数:
Stock k,l;
k.show();
l.show();
内联函数要求每个使用它的文件中都有它的实现代码。确保内联函数实现对多文件程序中的每个文件都可用,最简便的方法是:
将内联函数实现放在定义类的头文件中。
存储空间
创建的每个新对象都有自己的存储空间,用于存储其内部变量或者类成员。
但同一个类的所有对象共享同一组类函数。
比如k.shars和l.shars各占一个内存块,但k.show()和l.show()将执行同一个代码块
构造函数
由于类存在私有变量,所以无法像int类型那样在创建对象时初始化
Stock hot = {"abc",200,50.25}; //编译错误
类的构造函数用于创建对象时,自动对它进行初始化。
构造函数声明和实现:
构造函数的声明和实现有些特殊的点:①名称和类名相同 ②没有返回值,也没有声明为void类型
以Stock类为例,需要为Stock类提供3个值(total_val通过set_tot()计算获得,不需要设置),
如果只需要初始化company的话,也可以使用之前说过的默认参数,则声明为:
Stock(const string & co,long n = 0,double pr = 0.0);
//构造函数的实现可能是这样的:
Stock::Stock(const string &co,long n,double pr)
{
company = co;
if(n<0)
shares = 0;
else
shares = n;
share_val = pr;
set_tot();
}
注意没有返回类型,声明位于类声明的public部分。
那能不能图方便把实现中的参数名命名成和成员变量一样呢?
Stock::Stock(const string &company,long shares,double share_val) {...}
不能,因为在函数实现时你就会发现会出现shares = shares;这样的语句
为避免这种混轮,一种常见的做法是给参数加m_前缀,或者加_后缀:
Stock::Stock(const string &m_company,long m_shares,double m_share_val) {...}
Stock::Stock(const string &company_,long shares_,double share_val_) {...}
使用构造函数:
Stock food = Stock("World Cabbage",250,1.25); //显式调用
Stock garment("Furry Mason",50,2.5); //隐式调用
Stock *pStock = new Stock("Ele Games",18,19.0); //new时调用
需要指出的是:无法通过对象调用构造函数。
默认构造函数
默认构造函数是在未提供显示初始值时,用来创建对象的构造函数,即下述情况:
Stock fluffy_the_cat;
系统给的Stock的默认构造函数一般是:Stock::Stock() {}
需要注意的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
为类定义了构造函数后,程序员就必须为类再提供一个默认构造函数(而不是编译器自动提供),
否则会报错。
Stock(const string & co,long n = 0,double pr = 0.0); //单单定义自己的构造函数会报错
这样做可能是想禁止如下情况:创建了一个对象,而不显式地初始化。
Stock fluffy_the_cat; //我找到了名为Stock的构造函数却发现套不进去
自己定义默认构造函数方法有两种:
一种是给已有构造函数的所有参数提供默认值:
Stock(const string &co = "error",long n = 0,double pr = 0.0); //默认构造函数
//上述情况的话Stock fluffy_the_cat;就能套进去了
另一个是通过函数重载来定义另一个构造函数--一个没有参数的构造函数:
Stock(const string &co = "error",long n = 0,double pr = 0.0); //构造函数
Stock(); //默认构造函数
析构函数
析构函数在对象过期时自动调用,完成清理工作。
析构函数的声明和实现有些特殊的点:名称为~类名,没有返回值,也没有声明为void类型,没有参数
因此Stock的析构函数声明必须是这样的:
~Stock();
对应的实现为:
Stock::~Stock()
{
...
}
如果程序员没指定析构函数,则编译器也会生成一个空的析构函数。
完善后的Stock类
class Stock
{
private:
std::string company; //股票公司
long shares; //股票数
double share_val; //股价
double total_val; //股票价值
void set_tot() {total_val = shares*share_val;} //就地实现的方式
public:
Stock(); //默认构造函数
Stock(const string &co,long n = 0,double pr = 0.0); //构造函数
~Stock(); //析构函数
void acquire(const std::string & co,long n,double pr); //单使用声明的方式
void buy(long num,double price);
void sell(long num,double price);
void update(double price);
void show();
};
Stock::Stock() //默认构造函数
{
std::cout << "default constructor called \n";
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Stock::Stock(const string &co,long n,double pr)
{
std::cout << "constructor using" << co << "called \n";
company = co;
if(n < 0)
shares = 0;
else
shares = n;
share_val = pr;
set_tot();
}
Stock::~Stock()
{
std::cout << "Bye,"<<company << "!\n";
}
//其他函数的实现我就不写了
运算符重载
前面讲了函数的重载,C++中运算符也可以重载。
比如*即可以在声明中用于定义指针,也可以作为运算的乘法符号。
那么,如果用户想自定义运算符的用法,该怎么做呢?
比如,将两个数组的元素逐个相加,一般需要用到循环:
for(int i=0;i<20;i++)
evening[i] = sam[i] + janet[i];
但在C++中,可以定义一个代表数组的类,并重载+运算符:
evening = sam + janet;
重载运算符,需要用到被称为运算符函数的特殊函数形式,格式如下:
operatorop(argument list) //三部分operator+op+(argument list)
operator+() //重载+运算符
operator*() //重载*运算符
op必须是有效的运算符,不能有例如operator@()的函数,因为C++中没有@运算符
那么上述evening = sam + janet;可以这么实现:
#include<iostream>
class Stock
{
private:
int num[20];
public:
Stock(int a)
{
for(int i=0;i<20;i++)
num[i] = a;
}
Stock(){};
~Stock(){}; //析构函数
Stock operator+(const Stock &b)
{
Stock rt;
for(int i=0;i<20;i++)
rt.num[i] = this->num[i] + b.num[i];
return rt;
}
int show()
{
std::cout << this->num[1] << std::endl;
}
};
int main()
{
Stock c;
Stock a(1);
Stock b(2);
c = a + b;
c.show();
return 0;
}
执行c = a + b;时,编译器发现操作数是Stock类对象,于是执行了相应的运算符函数替换原运算符:
c = a.operator+(b);
友元函数
通常只有类的公有类函数能访问类的私有部分数据,C++提供了友元这种新的访问形式:
友元有三类:①友元函数 ②友元类 ③友元成员函数
此处讨论友元函数,通过让函数称为类的友元,可以赋予该函数与类成员函数相同的访问权限。
创建友元函数
将其声明放在类声明中,并在声明前加上关键字friend
friend int func(int a);
该原型意味着两点:
1.虽然func()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
2.虽然func()函数不是成员函数,但它与成员函数的访问权限相同
函数实现(不需要加Stock::,也不需要使用friend):
int func(int a)
{
...//可以访问Stock的private数据
}
虚函数
指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。
//举例:
#include<iostream>
using namespace std;
class A
{
public:
void print()
{
cout<<"This is A"<<endl;
}
};
class B : public A
{
public:
void print()
{
cout<<"This is B"<<endl;
}
};
int main()
{
//为了在以后便于区分,我这段main()代码叫做main1
A a;
B b;
a.print();
b.print();
return 0;
}
执行后得到:
“This is A”、“This is B”。
可以看出这两个class因个体的差异而采用了不同的策略,但这并不是多态性行为(使用的是不同类型的指针),
没有用到虚函数的功能。现在把main()处的代码改一改。
int main()
{
//main2
A a;
B b;
A *p1 = &a;
A *p2 = &b;
p1->print();
p2->print();
return 0;
}
运行一下看看结果,结果却是两个This is A(错)。
问题来了,p2明明指向的是class B的对象但却是调用的class A的print()函数,这不是我们所期望的结果,那么解决这个问题就需要用到虚函数。
class A
{
public:
virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
public:
void print(){cout<<"This is B"<<endl;}
};
毫无疑问,class A的成员函数print()已经成了虚函数,那么class B的print()成了虚函数了吗?
回答是Yes,我们只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。
所以,class B的print()也成了虚函数。那么对于在派生类的相应函数前是否需要用virtual关键字修饰,那就是你自己的问题了(语法上可加可不加,不加的话编译器会自动加上,但为了阅读方便和规范性,建议加上)。
现在重新运行main2的代码,这样输出的结果就是This is A和This is B了。