beesoft.org

C++. Wstep.

Zasady kodowania.

Zasady kodowania to zespol regul, uzywanych podczas pisania kodu zrodlowego programu.
Okreslaja one np. konwencje nazewnicze, jakie stosujemy w nadawaniu nazw zmiennym, glebokosc wciec czy sposob rozmieszczenia nawiasow.

Oczywiscie, mozna pisac bez zadnych regul, byle kod byl poprawny pod wzgledem wymogow jezyka C++ i program wynikowy tez bedzie dzialal. Z tym tylko, ze gdy program jest w miare duzy, nawet autor kodu bedzie mial po jakims czasie potezne problemy aby zrozumiec, o co mu chodzilo, gdy go kiedys pisal. Chyba nie ma sensu opisywać w jakim nastroju bedzie inny programista, ktory bedzie musial ten kod zmieniac lub rozszerzac. W takiej sytuacji lepiej byloby dla autora nie byc w zasiegu wzroku skazanca.

Oczywistym jest w takim razie, ze jakies reguly powinnismy stosowac.
W duzych grupach programistycznych jest to po prostu wymog formalny. W powaznych firmach software'owych swiezo zatrudniony programista pierwsze co dostaje do reki, to spisany dekalog regul kodowania obowiazujacych w firmie.

Zeby byla jasnosc. Nie ma jakiegos jedynie slusznego i uniwersalnego systemu kodowania.
Co firma, to inny zestaw regul. Co ksiazka, to inne reguly sa stosowane w przytaczanych przykladach.
I dobrze. Nie ma znaczenia jaki system regul bedziemy stosowac. Wazne jest, abysmy w ogole uzywali jakichs regul i abysmy byli w tym konsekwentni az do bolu.

Reguly przedstawione ponizej sa rezultatem moich wlasnych doswiadczen i przemyslen.
Ale bede tez prezentowac inne, alternatywne, czesto uzywane reguly.

W dalszej czesci tego dokumentu, opisujacej konkretne reguly, bede uzywal slowa 'powinien'. Oczywiscie 'powinien' w/g mnie. Reszta swiata ma prawo miec na ten temat inne zdanie.


1.4.1.Zasady nazywania plikow.

Pliki zawierajace definicje klasy powinny miec rozszerzenie 'cpp'.
Pliki zawierajace deklaracje klasy powinny miec rozszerzenie 'h'.
Deklaracja klasy moze byc dokonana tylko w jednym pliku 'h'.
Definicja klasy moze znajdowac sie tylko w jednym pliku 'cpp'.

Zalozmy, ze tworzymy klase o nazwie 'Widget'.
Deklaracja tej klasy powinna sie znajdowac w pliku 'Widget.h', natomiast definicja w 'Widget.cpp'.

1.4.2.Rozmiary.

Dlugosc lini tekstu w pliku zrodlowym nie moze byc dluzsza niz 80 znakow. Wynika to z wymogow czytelnosci kodu, zarowno na ekranie jak i na wydruku. Jesli kod pisany w linii zaczyna przekraczac 80 znakow nalezy go kontynuowac w nastepnej linii.

Definicja jednej funkcji nie powinna przekraczac 60 linii tekstu. Ta regula takze wynika z regul czytelnosci kodu. Jezeli funkcja staje sie zbyt dluga, nalezy wyodrebnic z niej podzadania i umiescic je w innych, dedykowanych funkcjach.

1.4.3.Naglowek pliku.

Kazdy plik powinien zawierac naglowek, w ktorym znajduja sie nastepujace informacje: nazwa projektu, copyright, nazwa firmy/grupy, nazwa pliku, autor pliku, data utworzenia pliku i krotki opis co plik zawiera. Opcjonalnie naglowek moze zawierac, co jest bardzo dobrym zwyczajem, historie zmian w pliku.
/********************************************************************
* Project        Sytem zarzadzania plikami BSCommander
* (c) copyright
* Organisation   Beesoft.org
* File           ViewWindow.cpp
* Author         Piotr Pszczolkowski (piotr@beesoft.org)
* Date           31.12.2010
* About          bla bla bla....
*--------------------------------------------------------------------
* History:
*   01.01.2011 - dodanie funkcji 'count' w celu zliczania lizakow,
*   12.02.2001 - zmiana typu zmiennej 'number' z 'int' na 'double'.
*   ...
*   ...
********************************************************************/
Oczywiscie, taki (lub podobny) naglowek stosujemy gdy piszemy program dla siebie (swojej firmy/organizacji). W przypadku gdy piszemy program na zewnatrz moze nam zostac narzucony inny, stosowny naglowek. Np. piszac program dla srodowiska Open Source na licencji GPL stosowny naglowek powinien miec postac:
/********************************************************************
 * Copyright (C) 2005 Piotr Pszczolkowski
 *-------------------------------------------------------------------
 * This file is part of BsC (Beesoft Commander).
 *
 * BsC is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * BsC is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with BsC; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *******************************************************************/

1.4.4.Pliki naglowkowe (include).

Standardowo deklaracja klasy znajduje sie w pliku naglowkowym z rozszerzeniem 'h'. To, ze plik zawierajacy implementacje klasy, plik z rozszerzeniem 'cpp', potrzebuje deklaracje wlasnej klasy jest oczywiste. Technicznie realizowane jest to przez polecenie preprocesora '#include<nazwa_pliku.h>'. Po napotkaniu takiej instrukcji preprocesor w miejscu jej wystapienia wstawi cala zawartosc zadanego pliku (jest to tzw. rozwiniecie w miejscu wywolania). Od tej chwili plik 'cpp' z wlaczonym plikiem 'h' podlega dalszej analizie jako tak zwana jednostka translacji.

Powiedzmy, ze istnieje juz klasa 'CWidget'.
Moze sie zdarzyc, ze takze implementacje wielu innych klas (np. CButton) moga potrzebować informacji o tym, jak zbudowna jest klasa 'CWidget'. Bedzie to mialo miejsce, gdy w tych klasach klasa 'CWidget' bedzie uzywana. Oznacza to za kazdym razem wczytywanie pliku 'CWidget.h'. Koniecznosc taka moze wystepować setki razy. A to jest czaaaaassss..... I dlatego plik naglowkowy musi miec na poczatku i na koncu odpowiednie instrukcje preprocesora:

#ifndef INCLUDED_WIDGET_H
#define INCLUDED_WIDGET_H

...

#endif // INCLUDED_WIDGET_H
Coz to oznacza?
Przy pierwszym odczycie pliku 'CWidget.h' preprocesor na samym poczatku natrafia na dyrektywe sprawdzenia czy znana jest definicja INCLUDED_WIDGET_H. Tylko pod warunkiem, ze nie jest znana moze czytac i analizowac dalsza zawartosc pliku. Oczywiscie definicja ta przy pierwszym odczycie nie jest znana. Wiec w tym momencie preprocesor taka definicje sobie tworzy i kontynuuje odczyt reszty pliku.
A co sie stanie gdy w jakims kolejnym, innym pliku ponownie wystapi dyrektywa wlaczenia pliku 'CWidget.h'? Ano nic, poniewaz preproceosor nie przebrnie przez #ifndef. INCLUDED_WIDGET_H juz jest mu znana. Warunek pozwalajacy na odczyt i analize nie jest spelniony. Preprocesor zignoruje reszte pliku az do dyrektywy #endif. Tzn. odczyta caly tekst pliku, ale to co znajduje sie przed #endif nie zostanie poddane zadnej analizie. A jakaz to oszczednosc czasu!!! Przeciez 'CWidget.h' moze chciec wczytywac dziesiatki lub setki innych plikow naglowkowych. No, ale teraz to juz sie nie zdarzy.

I kolejna sytuacja.
Zalozmy, ze po raz pierwszy chcemy wlaczyc 'CButton.h'. Ale sytuacja jest taka, ze na poczatku tego pliku znajduje sie dyrektywa wlaczajaca 'CWidget.h'. I co sie stanie? Ano normalnie przy wczytywaniu 'CButton.h' zostanie wczytany 'Widget.h' (choc byc moze nie bedzie amalizowany, ale tak czy inaczej bedzie wczytany). I to pomimo tego, ze 'Widget.h' zostal juz wczytany przy stu innych okazjach.
Rozwiazanie jest nastepujace:

#ifndef INCLUDED_BUTTON_H
#define INCLUDED_BUTTON_H

#ifndef INCLUDED_WIDGET_H
#include "Widget.h"
#endif // INCLUDED_WIDGET_H

...

#endif // INCLUDED_BUTTON_H
Teraz nawet przy pierwszym wczytaniu 'CButton.h', plik 'CWidget.h' zostanie wczytany tylko i wylacznie wtedy, gdy jeszcze tego wczesniej nie zrobiono. Jezeli wczesniej inne pliki wymusily wczytanie 'CWidget.h', to nawet przy pierwszym wczytywaniu 'CButton.h' dyrektywa wlaczajaca 'CWidget.h' zostanie zignorowana.

I ostatnia sytuacja. Dotyczaca nie naszych plikow naglowkowych. Czesto wlaczamy do naszych plikow pliki naglowkowe biliotek takich np. jak STL czy Qt. Bez sensu jest przed kazdym wlaczeniem dziesiatkow nieznanych plikow sprawdzac, jak jest okreslona definicja wlaczajaca. Wyjscie jest nastepujace: tworzymy wlasne definicje.

#ifndef INCLUDED_VECTOR
#include <vector>
#define INCLUDED_VECTOR
#endif // INCLUDED_VECTOR
Teraz bez wzgledu na to, ile razy w naszym programie umiescimy dyrektywe wlaczajaca plik naglowkowy <vector> zostanie wczytany tylko i wylacznie jeden raz.

I ostania uwaga. Wszystkie te sztuczki wpisujemy tylko i wylacznie w plikach naglowkowych.
W plikach 'cpp' piszemy grzecznie wszystkie potrzebne nam wlaczenia bez zadnych sprawdzen.

1.4.5.Jedna instrukcja w jednej linii.

Jak w tytule. W jednej linii wolno umieszczać tylko i wylacznie jedna instrukcje. Wiele instrukcji w tej samej linii jest potencjalnym zrodlem wielu bledow. Nie mowiac juz o tym, ze w przypadku gdy debugger wskaze linie z bledem nie wiadomo czego to dotyczy.
register( obj2 ); i++;	// Zle, latwo przeoczyc druga instrukcje

char* buffer1, buffer2;	// Zle, buffer2 nie bedzie wskaznikiem ale znakiem

1.4.6.Komentarze.

Komentarze w tekscie zrodlowym programu sa jak najbardziej porzadane. Zdecydowanie nalezy ich uzywac.
Maja one na celu opisanie rzeczy, ktore dla osoby nie bioracej udzialu w tworzeniu kodu, moga byc nieoczywiste. Programista powinien pisac komentarze takze z mysla o sobie. Za jakis czas moze tez miec problemy ze zrozumieniem wlasnego dziela.

Standard C++ dopuszcza dwa rodzaje komentarzy.
Przejety z C komentarz w postaci /* ... */.

int CWidget::get_counter()
{
   ++d_counter;
		
   /* 
    * To jest komentarz w stylu C.
    * Wszystko co tu sie znajduje nie bedzie brane pod
    * uwage przez kompilator, nie stanie sie czescia kodu
    * wynikowego naszego programu.
    * To jest tylko i wylacznie informacja dla programisty.
    * Jak widac moze znajdowac sie w wielu liniach.
    */
    
   return d_counter;
}	
oraz wlasny jednoliniowy komentarz //:
int CWidget::get_count()
{
	++d_counter;      // Komentarz w stylu C++. Zakres waznosci tylko do konca linii.
	return d_counter;
}
Osobiscie nie jestem zwolennikiem nadmiernej liczby komentarzy w kodzie programu. Oczywiscie powinny byc, ale w naprawde waznych, zeby nie powiedziec krytycznych, miejscach programu.
Nadmiar komentarzy zaciemnia calosciowy oglad kodu. Jestem zwolennikiem opisu pewnych szczegolow implementacyjnych w naglowkach funckji, czy nawet pliku. Ale opisy w komentarzach w tresci kodu nalezy ograniczyc do rozsadnego minimum. Co oczywiscie nie oznacza, ze nalezy z nich w ogole rezygnowac.

A juz cos takiego:
...
++d_counter;        // zwiekszam licznik o jeden
...
wola po prostu o zemste do nieba. Programista, ktory stosuje takie komentarze powinien byc w trybie natychmiastowym rozstrzelany przez swojego szefa.

Czyli: komentarze TAK, ale tam gdzie to jest naprawde niezbedne, a nie gdzie popadnie. Sensowne jest skomentowanie klasy w osobnym dokumencie, zawierajacym szczegoly implementacyjne, opisy zastosowanych algorytmow i diagramy UML.

I ostatnia uwaga. Stosuj wszedzie gdzie sie da komentarze C++. Komentarz C zostaw sobie jako mechanizm wylaczania czesci kodu w czasie testow programu. Wynika to z faktu, ze standardowo komentarze C nie moga sie zagniezdzac.

1.4.7.Nazwy zmiennych

Nazwy zmiennych powinny być czytelne i samokomentujace sie.
Oznacza to, ze najczesciej nazwa bedzie sie skladac z kilku wyrazow. I tu regula zapisu nazwy ma przynajmniej dwie szkoly. Wedlug mnie nazwa zmiennej powinna sie skladac z wyrazow pisanych malymi literami laczonymi znakiem podkreslnika:
...
int rows_number;
int cols_number;
int current_index;
CView* files_tree;
...
Jest to stara i dobra metoda zapisu jeszcze z czasow starego dobrego C. Stosowana powszechnie do dzisiaj, m.in. przez team tworzacy jadro Linuksa.

Inna, bardzo powszechna, jesli nie powszechniejsza, jest metoda zapisu przejeta z Javy. Zgodnie z nia, wyrazy tworzace nazwe zmiennej nie dzieli sie podkreslnikiem, ale zapisuje sie je duzymi literami (pierwsza litera zazwyczaj, choc tez nie zawsze, pozostaje mala):

...
int rowsNumber;
int colsNumber;
int currentIndex;
CView* filesTree;
...
Metoda jest o tyle dobra, ze nazwy zmiennych sa krotsze.

Czytelnosc znaczenia nazwy jest istotna gdy nie widzimy deklaracji zmiennej. W ramach ciala funkcji wymog ten nie jest juz tak istotny. Na pierwszy rzut oka widac deklaracje i latwo sie domyslec znaczenia zmiennej:

...
// wersja nr.1
for( int rows_counter = 0;  rows_counter < rows_number; ++rows_counter ) {
   ...
}
  
// wersja nr.2
for( int i = 0; i < rows_number; ++i ) {
}
...
Wedlug mnie lepsza jest wersja druga. Z kontekstu wynika jakie znaczenie ma zmienna sterujaca petla 'for'. Nadawanie jej nazwy opisowej jest nadmiarowe, wydluza tylko kod.

Nazwy zmiennych skladowych klas (class members).
Aby na pierwszy rzut oka moc rozroznic czy zmienna jest zmienna skladowa klasy, czy nie, nalezy dodawac na jej poczatku przedrostek 'd_'.

class CButton : public CWidget
{
	...
private:
	int d_width;
	int d_height;
	std::string d_txt;
	...
};
Jest szkola wywodzaca sie z Microsoftu nadajaca przedrostek m, np 'm_width' lub 'mWidth'.
Takze z Microsoftu wywodzi sie tzw. notacja wegierska, zgodnie z ktora kazda zmienna powinna na poczatku miec przedrostek okreslajacy jej typ: 'ptrArray', 'chElement' itd. Nie bede rozwijal tej kwestii, gdyz nie jestem jej zwolennikiem i jej nie stosuje.

Nazwy parametrow wywolania funkcji.
Kazda funkcja, takze konstruktory, moga miec (choc nie musza) tzw. parametry wywolania, Dodatkowo funkcja poprzez instrukcje return moze zwracac tylko JEDNA wartosc. Zarowno w funkcjach jak i konstruktorach czasami moga wystepowac konflikty nazewnicze pomiedzy parametrami wywolania a zmiennymi skladowymi klasy.
Dlatego tez:

...
CButton::CButton( const CWidget& in_parent )
{
   d_parent = in_parent;
   // gdyby nie przedrostek trzeba by kombinowac
   // z nazwa parametru. A tak wszystko jasne.
}

...

bool CButton::disp_image(
    const int in_x,         // przyslana wartosc tylko do odczytu
    const int in_y,         // przyslana wartosc tylko do odczytu
    handle& out_handle,     // tu jest adres do ktorego cos mamy wpisac
    string& inout_label )   // ta zmienna co przynosi, ale takze mamy cos do niej wpisac
{
}
...
W powyzszym przykladzie, w konstuktorze widac logike takich przedrostkow. Dla obiektu macierzystego (parent) nie trzeba wymyslac innych nazw dla parametrow wywolania i innych dla zmiennych klasy. Oba parametry nazywaja sie 'parent'. I jest to oczywiste i czytelne. Dzieki przedrostkom nie ma konfliktu nazw.
W przypadku funkcji sytuacja tez jest przejrzysta. Funkcja zwraca wartosc 'bool', ktora np. okresla czy powiodlo sie wyswietlenie np. ikony.

1.4.8.Stosowanie nawiasow klamrowychi.

Nawiasy klamrowe '{' i '}' sluza do okreslania m.in. cial funkcji, czy grupowania instrukcji.

1.4.9.Wciecia.

W celu zwiekszenia czytelnosci kodu i jego logicznej struktury stosujemy wciecia.
Dotyczy to zarowno deklaracji klas, definicji funkcji jak i grup instrukcji.
Im wieksze wciecia tym lepiej, ale bez przesady. Nalezy wziasc pod uwage, ze szerokosc strony (na ekranie czy drukarce) jest ograniczona. Nalezy znalesc kompromis pomiedzy stosowanymi wcieciami, a calosciowym wygladem strony.
Zwyczajowo stosuje sie wciecia o glebokosci od 3 do 5 znakow.
Osobiscie preferuje dolna granice, czyli 3,4 znaki.
Contact: piotr@beesoft.org
(C) 2006-2008 beesoft.org
Last modification date: 2008-06-29
Visitis counter:
counter of visits