返回   cpper编程论坛 > 技术杂烩
注册账号 论坛帮助 会员列表 日历事件 搜索 今日新帖 标记版面已读

技术杂烩 找不到地方的技术问题?这里!

关闭主题
 
LinkBack 主题工具 显示模式
  #1 (permalink)  
旧 2003-11-19
codinggirl 的头像
高级会员
 
注册日期: 2002-09-16
帖子: 1,052
codinggirl 正向着好的方向发展
默认 allaboutprogram.com C++ 初学者FAQ

Purpose :这是一份对于C++初学者来说需要熟练掌握的基础知识文档
Plan on updating :我会先写一份,大家有兴趣的话参加讨论,等讨论和意见结束我整理成FAQ,并将之放在本置顶贴里,供以后本论坛的同仁学习参阅。而且我会根据本坛所提出的问题进行总结,不定期的更新和添加内容。学疏才浅,难免有不周或错误之处,如果发现错误,请及时与我联系。谢谢大家的支持,我会不断改进。非常欢迎各位有兴趣的同志添加内容,同样的办法,在大家讨论结束后,我会整理并加入本FAQ。有意者请利用论坛消息联系我。
Acknowledgement :在此特别感谢abp,pora和各位参与讨论的同仁对我的支持和鼓励。
Copyright :http://www.allaboutprogram.com网站 2003, all right reserved
Digg this Post!Add Post to del.icio.usBookmark Post in TechnoratiFurl this Post!
  #2 (permalink)  
旧 2003-11-25
codinggirl 的头像
高级会员
 
注册日期: 2002-09-16
帖子: 1,052
codinggirl 正向着好的方向发展
默认 allaboutprogram.com C++ 初学者FAQ

1.关于FAQ的更新:
我会在“某天”给出一篇文章,贴在这里,一周时间就此文章讨论和收集好的提议和观点,一周(左右)后由我(或本文的作者)总结大家的发言和提议,我会将总结性的文章附上作者的名字贴在这里,供大家学习。(这里叫做FAQ是因为我现在想不出更好的叫法:ppp)
为避免关于文章的观点“众说纷纭”,而导致杂乱,我locked住此贴,如果大家有好的建议或提议,请到相关文章的帖子里回复。

2.关于FAQ的内容:
本FAQ的内容立足基础点,提交讨论的源文章:一篇文章基于一个知识点展开,重点讲解总结相关于本知识点的的基础知识,但源文章也需要有一定深度的探索。而文章内容不一定为FAQ的形式。但为保证本FAQ的严肃性,请不要以小品文等诙谐的方式出现,必须做到严谨并无废话。


PS:如果大家关于这个公益性的帖子有好的建议或要求,可通过站内信件或我的mail(coding_girl@163.com)通知我,欢迎大家踊跃写文章(这可是造福千秋的事业,欢迎大家踊跃踊跃再踊跃
Digg this Post!Add Post to del.icio.usBookmark Post in TechnoratiFurl this Post!
  #3 (permalink)  
旧 2003-11-25
codinggirl 的头像
高级会员
 
注册日期: 2002-09-16
帖子: 1,052
codinggirl 正向着好的方向发展
默认 allaboutprogram.com C++ 初学者FAQ

//Author :CodingGirl
//Thanks :在此感谢Innocentius和众位热心朋友对某些知识点的指导和修正
//Copyright :http://www.allaboutprogram.com 2003, all right reserved

FAQ 1. 对象初始化-----构造函数,拷贝构造函数和operator =

一个具有良好结构骨干的,明确意义和定义良好的class,是对象间良好合作的重要条件(强调类设计的内聚性和完整性),为了做到这一点在设计类时类的拷贝构造、析构和赋值是类设计者应该考虑的;什么时候应该仔细考虑他们呢?一个基本准则即是:当一个class里有动态配置的内存(或有指向其他不可共享的资源)的时候,请为此class手动撰写拷贝构造(copy constructor)、析构(dtor)和赋值(operator = )函数; 不管class符不符合这个基本准则,也请你确信已经考虑过他们

首先分清initialization(初始化)和assignment(赋值),对象的初始化发生在它初次获得一个值的时候,对于带有constructors(构造函数)的class或struct,初始化会调用某个构造函数完成。这和assignment不同,assignment发生在“已初始化的对象被赋予新值”,所以区分下列动作:
代码:
string s1; // initialization(初始化)调用constructor(构造函数) string s2(“Hello”); // initialization(初始化)调用constructor(构造函数) string s3 = s2; //initialization(初始化)调用copy constructor(构造函数) string s4(s2); //initialization(初始化)调用copy constructor(构造函数) s1 = s3; // assignment(赋值) 调用operator =
C++中严格区分initialization(初始化)和assignment(赋值),assignment动作的目标对象不是尚未构造完的对象,而是可能已经拥有配置好的资源(这里资源指内存,句柄,等需要动态配置的),在新资源A被赋值给此类的其他已被配置好资源的对象B之前,B的旧资源必须先释放。参见如下代码:
代码:
//--------------- here is constructor and defult constructor -------- //--------------- the defult namePtr value = NULL ------- Man(const char *name = NULL ):namePtr(NULL) { if(name) // 也可以用if (name != NULL) { namePtr = new char[strlen(name) + 1]; strcpy(namePtr,name); } }; //---------------here is dtor ------------------------------- ~Man() { delete [] namePtr; }; //--------------here is copy ctor ------------------------------ Man(const Man &rhs):namePtr(NULL) { if(rhs.namePtr) //也可以用if( rhs.namePtr != NULL) { namePtr = new char[strlen(rhs.namePtr) + 1]; strcpy(namePtr , rhs.namePtr); } } //-------------here is operator = -------------------------------- Man & operator = (const Man & rhs) { if(namePtr == rhs.namePtr) return *this; if(rhs.namePtr) //也可以用if( rhs.namePtr != NULL) { char * name=new char[strlen(rhs.namePtr)+1]; strcpy(name,rhs.namePtr); delete [] namePtr; namePtr=name; } else { delete [] namePtr; namePtr = NULL; } return *this; } private: char * namePtr; };
再看下面的代码:
代码:
class Man() { public: Man(const char *name); ~Man(); //这里没有copy ctor和operator= private: char *namePtr; };
一个需要动态配置内存的类里没有copy ctor和operator= 会带来严重的后果。
需要记住的一点就是,当你没有显式的提供这两个函数的时候,C++会产生一个默认的copy ctor和operator=并调用之,而且默认产生的operator=会执行memberwise assigment(一个一个的拷贝),对指针而言就是按位拷贝(bitwise copy)。
如果这样用Man定义:
Man man_1("CodingGirl");
Man man_2("Programmer");
man_1 = man_2; //由于operator=这个动作,原class作者并没有给出定义,所以编译器会产生默认的operator =,并将man_2的内容逐个赋值给man_1相对应的member。这种动作对于指针而言是很危险的,那意味着man_1和man_2中各自的namePtr都指向含有字符串“Programmer”的这块内存。这样做的后果是:1.man_1原先所指的内存(存放"CodingGirl"的那块)由于没有delete,所以永远的丢失了,这就是所谓的memory leak(内存泄漏)。2. man_1 , man_2中各自的指针namePtr都指向相同的字符串,当其中一个,比如man_1先离开其作用域范围时,先离开的这个对象man_1会调用自己的dtor删除这块内存,但这块内存仍然被另一个man_2的指针所指,这是一个很危险的动作。比如:
代码:
Man man_1("CodingGirl"); if(....) { Man man_2("Programmer"); man_2 = man_1; //执行默认的operator=,man_2.namePtr,man_1.namePtr 同时指向"CodingGirl"这块内存 man_2的内存泄漏} //}后离开生存空间,调用man_2 dtor,"CodingGirl"这块内存被删除,但man_1.namePtr还是指向这块内存 Man man_3 = man_1; //man_1.namePtr所指向的内存已经被删除,所以man_1.namePtr未定义,由此,man_3.namePtr未定义;
没有显式提供copy ctor而使用默认提供的情况和上面类似。

当你熟悉了所有这一切之后,可以提炼ctor,copy ctor和dtor中共同的初始化部分
而改成下列的代码:
代码:
class Man{ public: // constructor Man(const char * name=NULL) :namePtr(NULL){ if(name){ namePtr=new char[strlen(name)+1]; strcpy(namePtr,name); } }; // copy constructor Man(const Man & rhs) :namePtr(NULL){ Assign(rhs); }; // destructor ~Man() throw(){ delete [] namePtr; }; // operator = Man & operator = (const Man & rhs){ return Assign(rhs); }; // helper function Man & Assign(const Man & rhs){ if(namePtr==rhs.namePtr) return *this; if(rhs.namePtr==NULL){ delete [] namePtr; namePtr=NULL; } else{ char * new_namePtr=new char[strlen(rhs.namePtr)+1]; strcpy(new_namePtr,rhs.namePtr); delete [] namePtr; namePtr=new_namePtr; } return *this; }; private: char * namePtr; };
当你的class不需要提供copy ctor和operator=的时候,把他们声明为private,并不要对他们进行定义。这样既阻止了class的对象调用他们,也可以避免编译器自动产生他们。
以上问题参阅书籍:
The C++ Program Language (Bjarne Stroustrup(简称:TCPL))
ISO 14882 Programming Languges -- C++ ( C++标准委员会(ISO C++标准))
C++ Primer (Stanley B.Lippman)
C++ Primer(3/e) Answer Book
Effective C++ (Scott Meyers)
More Effective C++ (Scott Meyers)
C++ FAQs (2nd Edition) (Marshall cline)


关于FAQ01内容的相关讨论请详见这里:
http://www.allaboutprogram.com/bb/viewtopic.php?t=1186
__________________
世纷纷兮,心胡乱
霜寒寒兮,魂不归
诚烈烈兮,愁不化
Digg this Post!Add Post to del.icio.usBookmark Post in TechnoratiFurl this Post!
  #4 (permalink)  
旧 2003-12-31
codinggirl 的头像
高级会员
 
注册日期: 2002-09-16
帖子: 1,052
codinggirl 正向着好的方向发展
默认 allaboutprogram.com C++ 初学者FAQ

ABP C++ FAQ02-------------essential const


常变量, const和变量
1.const int x = 8;
常指针 ,const和指针
2.const int *ptr = &x ;
3.int const *ptr = &x ;
4.int * const ptr = &x ;
5.const int * const ptr = &x;
const和引用
6.const int &i= x; 和int const &j = y;
常函数 const和函数
7.const int func(int p)
8.const int *func(int *p);
9.int func (int p) const;
10.int func (const int *p);
11.const int func (const int *p) const
常对象
12.常对象const A a , A const a;
13.mutable关键字


1.const int x = 8;
const限定修饰符把一个对象转换成一个常量,所以现在x是只读的(read-only)。
2.const int *ptr = &x ;
这里const修饰的是*ptr ,ptr是一个指向int类型的,被定义成const的对象的指针。这意味着,可以赋给ptr其他变量的地址值,但不能修改ptr指向的值。被定义成const对象的指针可以指向常量或非常量的对象。即:
代码:
int x = 255; //非常量对象 const int y = 512; //常量对象 const int *xPtr = &x ; //正确 const int *yPtr = &y ; //正确
被定义成const对象的指针的意义就是:不允许通过指针间接地改变源对象的值。即:
代码:
*xPtr = 512; //错误,不能修改xptr指向的值,即不能通过指向常量的 //const对象的指针间接改变其所指向的对象 x = 512; //正确,直接修改非const对象的值 yPtr = xPtr; //正确,可以赋给yPtr其他变量的地址值 xPtr = yPtr; //正确,可以赋给xPtr其他变量的地址值 int z = *yPtr; //正确,这只是值拷贝
注意点:
const对象的地址只能赋值给指向const对象的指针。

3.int const *ptr = &x ;
除了const的位置不同,作用,意义和2里相同。

4.int * const ptr = &x ;
这里const修饰的是ptr,ptr是一个指向int类型对象的const指针。这意
味着,不能赋给ptr其他变量的地址值,但可以修改ptr指向的值。所以
const常量对象不能被赋给同类型的const指针,即:
代码:
int x = 255; //非常量对象 int z = 768; //非常量对象 const int y = 512; //常量对象 int * const xPtr = &x; //正确 xPtr = &z; //错误,反过来想,假如const指针可以间接的修改其指向的值,那么就和常量对象不能被修改的定义矛盾了。 int * const yPtr = &y; //错误,const int * 和 int * const是两个完全不同的类型,所以不能这样初始化。 int val = *xPtr; //正确,这只是值拷贝
5.const int * const ptr = &x;
把3,4结合起来

6.const int &i= x; 和int const &j = y;
引用实际就是变量的别名,初始化后,在别名的生存期内,只能和其变量本身关联而不能再指向其它变量,任何对引用的改变都将改变原变量。引用和变量本身是指向同一内存地址的。用const修饰引用,const加在数据类型前后均可,即:
const int &i= x;和int const &j = y;虽然形式不同,但实质一样
代码:
int x = 255; int y = 512; const int &xRef = x; //对象变量的引用在声明时做初始化 int const &yRef = y; //对象变量的引用在声明时做初始化 xRef = 768; //错误,常引用不能改变内容 yRef = 1024; //错误,常引用不能改变内容 x = 128; // x和xRef都等于128,任何对引用的改变都将改变原变量 y = 768; // y和yRef都等于128,任何对引用的改变都将改变原变量 int z = xRef; //正确,这只是值拷贝
7.const int func(int p)
func函数返回一个常量对象。这个返回值可以赋给const或非const对象,因为他返回的是一份拷贝。
代码:
const int func( int p) { return p; } int retval = func(9); //正确 const int retval = func(9); //正确
注意:但事实上这样的返回,是否加const是没有意义的。
8.const int *func(int *p);
func函数返回一个常量地址。
代码:
const int *func(int *p) { return p; } int x = 255; int * xPtr = &i; const int *retval = func(xPtr); //正确 int *retval = func(xPtr); //错误
9.int func (int p) const;
常函数,这种形式只能用于修饰类的成员函数,作用:在常函数中不能改变本类数据成员值,不能用于全局函数和静态成员函数。比如:
代码:
class A { … int func ()const { //data = 6; //错误,不能在常函数中修改本类数据成员的值 return data; } … private: int data; } 常函数只能调用常函数,但常函数可以被任意函数(常函数/非常函数)调用。 class A { … void func1 ()const; void func2 ()const; void func3 (); … private: int data; } void A::func1()const { cout << "This is func1" << endl; } void A::func2()const { func1(); //正确,常函数可以调用常函数 //func3(); //错误,常函数只能调用常函数 cout << "This is func2" << endl; } void A::func3() { func2(); //正确,常函数可以被任意函数调用 cout << "This is func3" << endl; }
常量对象只能调用该对象的常函数。
注意点:可以把非const成员函数重载为const成员函数,反之亦然。
10.int func (const int *p);
修饰func函数的参数,使得参数不被在函数体func内修改。
11.const int func (const int *p) const
把7,8,9结合起来
12.常对象const A a , A const a;
const修饰对象,const加在数据类型前后均可,const对象只能调用该对象的const成员函数。
13.Mutable关键字。
提到const不得不提一下mutable。看如下的例子:
代码:
class A { public: ……………… string func() const { if(age >= 50) elder.push_back(name); //错误,在const成员函数里不能修改本类的成员变量 return name; } ………………… private: string name; int age; vector<string> elder; };
这里当在const成员函数里不得不去修改一些成员变量的时候,显然这个func的实现是不符合要求的,那么用mutable来解决这个问题,当对非静态数据成员运用mutable时,这些成员的“bitwise constness”限制就被解除。将
代码:
vector<string> elder;
改为:
代码:
mutable vector<string> elder;即可
代码:
class A { public: ……………… string func() const { if(age >= 50) elder.push_back(name); //正确,通过mutable修饰的成员变量可以在本类的const成员函数中被修改 return name; } ………………… private: string name; int age; mutable vector<string> elder; //数据成员现在为mutable;他可以在任何地方被修改,包括在const成员函数里 }; 注意点:mutable只能应用于修饰非静态数据成员。
关于FAQ02内容的相关讨论请详见这里:
http://www.allaboutprogram.com/bb/viewtopic.php?t=1329
__________________
世纷纷兮,心胡乱
霜寒寒兮,魂不归
诚烈烈兮,愁不化
Digg this Post!Add Post to del.icio.usBookmark Post in TechnoratiFurl this Post!
  #5 (permalink)  
旧 2004-03-22
codinggirl 的头像
高级会员
 
注册日期: 2002-09-16
帖子: 1,052
codinggirl 正向着好的方向发展
默认 C++中的内部连接与外部连接(上)

FAQ03:C++中的内部连接与外部连接(上)
-----作者:SpitFire

代码:
在说内部连接与外部连接前,先说明一些概念。 1.声明 一个声明将一个名称引入一个作用域; 在c++中,在一个作用域中重复一个声明是合法的 以下都是声明: int foo(int,int); //函数前置声明 typedef int Int; //typedef 声明 class bar; //类前置声明 extern int g_var; //外部引用声明 class bar; //类前置声明 typedef int Int; //typedef 声明 extern int g_var; //外部引用声明 friend test; //友员声明 using std::cout; //名字空间引用声明 friend test; //友员声明 using std::cout; //名字空间引用声明 int foo(int,int); //函数前置声明 在同一个作用域中你可以多次重复这些声明。 有两种声明不能重复,那就是类成员函数及静态数据成员的声明 class foo { static int i; static int i;//不可以 public: int foo(); int foo();//不可以 }; 2.定义 一个定义提供一个实体(类型、实例、函数)在一个作用域的唯一描述。 在同一作用域中不可重复定义一个实体。 以下都是定义。 int y; class foo {...}; struct bar {...}; foo* p; static int i; enum Color{RED,GREEN,BLUE}; const double PI = 3.1415; union Rep{...}; void test(int p) {}; foo a; bar b; 3.编译单元 当一个c或cpp文件在编译时,预处理器首先递归包含头文件,形成一个含有所有 必要信息的单个源文件,这个源文件就是一个编译单元。这个编译单元会被编译成为一个与cpp文件名同名的目标文件(.o或是.obj)。连接程序把不同编译单元中产生的符号联系起来,构成一个可执行程序。 4.自由函数 如果一个函数是自由函数,那么这个函数不是类的成员函数,也不是友元函数。 下面来看内部连接和外部连接 内部连接:如果一个名称对于它的编译单元来说是局部的,并且在连接时不会与其它编译单元中的同样的名称相冲突,那么这个名称有内部连接(注:有时也将声明看作是无连接的,这里我们统一看成是内部连接的)。 以下情况有内部连接: a)所有的声明 b)名字空间(包括全局名字空间)中的静态自由函数、静态友元函数、静态变量的定义 c)enum定义 d)inline函数定义(包括自由函数和非自由函数) e)类的定义 f)名字空间中const常量定义 g)union的定义 外部连接:在一个多文件程序中,如果一个名称在连接时可以和其它编译单元交互,那么这个名称就有外部连接。 以下情况有外部连接: a)类非inline函数总有外部连接。包括类成员函数和类静态成员函数 b)类静态成员变量总有外部连接。 c)名字空间(包括全局名字空间)中非静态自由函数、非静态友元函数及非静态变量 下面举例说明: a)声明、enum定义、union定义有内部连接 所有的声明、enum定义及union定义在编译后不会产生连接符号,也就是在不同编译单元中有相同名称的声明及enum、union定义并不会在连接时发生发现多个符号的错误。 // main.cpp typedef int Int; //typedef 声明,内部连接 enum Color{red}; //enum定义,内部连接 union X //union定义,内部连接 { long a; char b[10]; }; int main(void) { Int i = red; return i; } // a.cpp typedef int Int; //在a.cpp中重声明一个int类型别名,在连接时不会发生错误 enum Color{blue}; //在a.cpp中重定义了一个enum Color,在连接时不会发生错误 const Int i =blue; //const常量定义,内部连接 union X //union定义,内部连接 { long a; char b[10]; }; b)名字空间中静态自由函数、静态友元函数、静态变量、const常量定义有内部连接 // main.cpp namespace test { int foo(); //函数声明,内部连接 static int i = 0; //名字空间静态变量定义,内部连接 static int foo() { return 0;} //名字空间静态函数定义,内部连接 } static int i = 0; //全局静态变量定义,内部连接 static int foo() {return 1;} //全局静态函数定义,内部连接 const int k = 0; //全局const常量定义,内部连接 int main(void) { return 0; } //a.cpp namespace test { int i = 0; //名字空间变量定义,外部连接 int foo() {return 0;} //名字空间函数定义,外部连接 } int i = 0; //全局变量定义,外部连接 int k = 0; //全局变量定义,外部连接 int foo() { return 2;} //全局函数定义,外部连接 在全局名字空间中,main.cpp中定义了静态变量i,常量k,及静态自由函数foo等,这些都有内部连接。如果你将这些变量或函数的static或是const修饰符去掉,在连接时就会现multiply defined symbols错误,它们与a.cpp中的全局变量、全局函数发生冲突。 c)类定义总有内部连接,而非inline类成员函数定义总有外部连接,不论这个成员函数是静态、虚拟还是一般成员函数,类静态数据成员定义总有外部连接。 1.类的定义有内部连接。如果不是,想象一下你在4个cpp文件中include定义了类Base的头文件,在4个编译单元中的类Base都有外部连接,在连接的时候就会出错。 看下面的例子: //main.cpp class B //类定义,内部连接 { static int s_i; //静态类成员声明,内部连接 public: void foo() { ++s_i;} //类inline函数,内部连接 }; struct D { void foo(); //类成员函数声明,内部连接 }; int B::s_i = 0; //类静态数据成员定义,外部连接 void D::foo() //类成员函数定义,外部连接 { cout << "D::foo in main.cpp" <<endl; } int main() //main函数,全局自由函数,外部连接 { B b; D d; return 0; } //a.cpp class B { int k; }; struct D { int d; }; 在这个例子中,main.cpp与a.cpp中都有class B和class D的定义,但在编译这两个cpp文件时并不发生link错误。 2.类的非inline成员函数(一般,静态,虚拟都是)总有外部连接,这样当你include了某个类的头文件,使用这个类的函数时,就能连接到正确的类成员函数上,继续以上面为例子,如果把a.cpp中的struct D改为 struct D //类定义 { int d; void foo(); //类成员函数声明 }; void D::foo() //类成员函数定义,外部连接 { cout << " D::foo in a.cpp" <<endl; } 这时main.cpp与a.cpp中的D::foo都有外部连接,在连接就会出现multiply defined symbols错。 3.类的静态数据成员有外部连接,如上例的B::s_i,这样当你在main.cpp中定义了类静态数据成员,其它编译单元若使用了B::s_i,就会连接到main.cpp对应编译单元的s_i。 d)inline函数总有内部连接,不论这个函数是什么函数 // main.cpp inline int foo() { return 1;} //inline全局函数,内部连接 class Bar //类定义,内部连接 { public: static int f() { return 2;} //inline 类静态函数,内部连接 int g(int i) { return i;} //inline 类成员函数,内部连接 }; class Base { public: inline int k(); //类成员函数声明,内部连接 }; inline int Base::k(){return 5;} //inline 类成员函数,内部连接 int main(void) { return 0; } 如果你的Base类是定义在Base.h中,而Base的inline 函数是在Base.cpp中定义的,那么在main.cpp中include "Base.h"编译不会出现问题,但在连接时会找不到函数k,所以类的inline函数最好放到头文件中,让每一个包含头文件的cpp都能找到inline函数。 现在对c++中的连接有了一个认识,能清楚的知道是什么原因产生连接时错误。当你在连接时产生连接不到的错误,这说明所有的编译单元都没有这个实体的外部连接;当你在连接时发现有多个连接实体,这说明有多个编译单元提供了同名的有外部连接的实体。同时,在进行程序设计时,也要注意不要使只有本编译单元用到的函数、类、变量等有外部连接,减少与其它编译单元的连接冲突。 不过在这里没有说明template函数及template class的连接性,并且对一些特别的情况也没有作出说明(比如inline函数不能被inline),欲知后事如何,且听下回分解 :)。
关于FAQ03的相关讨论,详见以下链接:
http://www.allaboutprogram.com/bb/viewtopic.php?t=1612
Digg this Post!Add Post to del.icio.usBookmark Post in TechnoratiFurl this Post!
关闭主题

书签

主题工具
显示模式

发帖规则
不可以发表新主题
不可以发表回复
不可以上传附件
不可以编辑自己的帖子

启用 BB 代码
论坛启用 表情符号
论坛启用 [IMG] 代码
论坛禁用 HTML 代码
Trackbacks are 启用
Pingbacks are 启用
Refbacks are 启用



所有时间均为格林尼治时间 +9。现在的时间是 11:44 PM


Powered by vBulletin® 版本 3.7.0
版权所有 ©2000 - 2009,Jelsoft Enterprises Ltd.
(C) Copy Right All Right Reserved 2001 - 2007

Search Engine Friendly URLs by vBSEO 3.1.0