C. Didattica e programmazione [quarta ed.] 9788871922195

Il testo di Al Kelley e Ira Pohl si conferma come una guida completa e aggiornata, per l'apprendimento di un lingua

318 32 198MB

Italian Pages 672 [669] Year 2004

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

C. Didattica e programmazione [quarta ed.]
 9788871922195

Table of contents :
Scan-201105-0002_1L
Scan-201105-0002_2R
Scan-201105-0003_1L
Scan-201105-0003_2R
Scan-201105-0004_1L
Scan-201105-0004_2R
Scan-201105-0005_1L
Scan-201105-0005_2R
Scan-201105-0006_1L
Scan-201105-0006_2R
Scan-201105-0007_1L
Scan-201105-0007_2R
Scan-201105-0008_1L
Scan-201105-0008_2R
Scan-201105-0009_1L
Scan-201105-0009_2R
Scan-201105-0010_1L
Scan-201105-0010_2R
Scan-201105-0011_1L
Scan-201105-0011_2R
Scan-201105-0012_1L
Scan-201105-0012_2R
Scan-201105-0013_1L
Scan-201105-0013_2R
Scan-201105-0014_1L
Scan-201105-0014_2R
Scan-201105-0015_1L
Scan-201105-0015_2R
Scan-201105-0016_1L
Scan-201105-0016_2R
Scan-201105-0017_1L
Scan-201105-0017_2R
Scan-201105-0018_1L
Scan-201105-0018_2R
Scan-201105-0019_1L
Scan-201105-0019_2R
Scan-201105-0020_1L
Scan-201105-0020_2R
Scan-201105-0021_1L
Scan-201105-0021_2R
Scan-201105-0022_1L
Scan-201105-0022_2R
Scan-201105-0023_1L
Scan-201105-0023_2R
Scan-201105-0024_1L
Scan-201105-0024_2R
Scan-201105-0025_1L
Scan-201105-0025_2R
Scan-201105-0026_1L
Scan-201105-0026_2R
Scan-201105-0027_1L
Scan-201105-0027_2R
Scan-201105-0028_1L
Scan-201105-0028_2R
Scan-201105-0029_1L
Scan-201105-0029_2R
Scan-201105-0030_1L
Scan-201105-0030_2R
Scan-201105-0031_1L
Scan-201105-0031_2R
Scan-201105-0032_1L
Scan-201105-0032_2R
Scan-201105-0033_1L
Scan-201105-0033_2R
Scan-201105-0034_1L
Scan-201105-0034_2R
Scan-201105-0035_1L
Scan-201105-0035_2R
Scan-201105-0036_1L
Scan-201105-0036_2R
Scan-201105-0037_1L
Scan-201105-0037_2R
Scan-201105-0038_1L
Scan-201105-0038_2R
Scan-201105-0039_1L
Scan-201105-0039_2R
Scan-201105-0040_1L
Scan-201105-0040_2R
Scan-201105-0041_1L
Scan-201105-0041_2R
Scan-201105-0042_1L
Scan-201105-0042_2R
Scan-201105-0043_1L
Scan-201105-0043_2R
Scan-201105-0044_1L
Scan-201105-0044_2R
Scan-201105-0045_1L
Scan-201105-0045_2R
Scan-201105-0046_1L
Scan-201105-0046_2R
Scan-201105-0047_1L
Scan-201105-0047_2R
Scan-201105-0048_1L
Scan-201105-0048_2R
Scan-201105-0049_1L
Scan-201105-0049_2R
Scan-201105-0050_1L
Scan-201105-0050_2R
Scan-201105-0051_1L
Scan-201105-0051_2R
Scan-201105-0052_1L
Scan-201105-0052_2R
Scan-201105-0053_1L
Scan-201105-0053_2R
Scan-201105-0054_1L
Scan-201105-0054_2R
Scan-201105-0055_1L
Scan-201105-0055_2R
Scan-201105-0056_1L
Scan-201105-0056_2R
Scan-201105-0057_1L
Scan-201105-0057_2R
Scan-201105-0058_1L
Scan-201105-0058_2R
Scan-201105-0059_1L
Scan-201105-0059_2R
Scan-201105-0060_1L
Scan-201105-0060_2R
Scan-201105-0061_1L
Scan-201105-0061_2R
Scan-201105-0062_1L
Scan-201105-0062_2R
Scan-201105-0063_1L
Scan-201105-0063_2R
Scan-201105-0064_1L
Scan-201105-0064_2R
Scan-201105-0065_1L
Scan-201105-0065_2R
Scan-201105-0066_1L
Scan-201105-0066_2R
Scan-201105-0067_1L
Scan-201105-0067_2R
Scan-201105-0068_1L
Scan-201105-0068_2R
Scan-201105-0069_1L
Scan-201105-0069_2R
Scan-201105-0070_1L
Scan-201105-0070_2R
Scan-201105-0071_1L
Scan-201105-0071_2R
Scan-201105-0072_1L
Scan-201105-0072_2R
Scan-201105-0073_1L
Scan-201105-0073_2R
Scan-201105-0074_1L
Scan-201105-0074_2R
Scan-201105-0075_1L
Scan-201105-0075_2R
Scan-201105-0076_1L
Scan-201105-0076_2R
Scan-201105-0077_1L
Scan-201105-0077_2R
Scan-201105-0078_1L
Scan-201105-0078_2R
Scan-201105-0079_1L
Scan-201105-0079_2R
Scan-201105-0080_1L
Scan-201105-0080_2R
Scan-201105-0081_1L
Scan-201105-0081_2R
Scan-201105-0082_1L
Scan-201105-0082_2R
Scan-201105-0083_1L
Scan-201105-0083_2R
Scan-201105-0084_1L
Scan-201105-0084_2R
Scan-201105-0085_1L
Scan-201105-0085_2R
Scan-201105-0086_1L
Scan-201105-0086_2R
Scan-201105-0087_1L
Scan-201105-0087_2R
Scan-201105-0088_1L
Scan-201105-0088_2R
Scan-201105-0089_1L
Scan-201105-0089_2R
Scan-201105-0090_1L
Scan-201105-0090_2R
Scan-201105-0091_1L
Scan-201105-0091_2R
Scan-201105-0092_1L
Scan-201105-0092_2R
Scan-201105-0093_1L
Scan-201105-0093_2R
Scan-201105-0094_1L
Scan-201105-0094_2R
Scan-201105-0095_1L
Scan-201105-0095_2R
Scan-201105-0096_1L
Scan-201105-0096_2R
Scan-201105-0097_1L
Scan-201105-0097_2R
Scan-201105-0098_1L
Scan-201105-0098_2R
Scan-201105-0099_1L
Scan-201105-0099_2R
Scan-201105-0100_1L
Scan-201105-0100_2R
Scan-201105-0101_1L
Scan-201105-0101_2R
Scan-201105-0102_1L
Scan-201105-0102_2R
Scan-201105-0103_1L
Scan-201105-0103_2R
Scan-201105-0104_1L
Scan-201105-0104_2R
Scan-201105-0105_1L
Scan-201105-0105_2R
Scan-201105-0106_1L
Scan-201105-0106_2R
Scan-201105-0107_1L
Scan-201105-0107_2R
Scan-201105-0108_1L
Scan-201105-0108_2R
Scan-201105-0109_1L
Scan-201105-0109_2R
Scan-201105-0110_1L
Scan-201105-0110_2R
Scan-201105-0111_1L
Scan-201105-0111_2R
Scan-201105-0112_1L
Scan-201105-0112_2R
Scan-201105-0113_1L
Scan-201105-0113_2R
Scan-201105-0114_1L
Scan-201105-0114_2R
Scan-201105-0115_1L
Scan-201105-0115_2R
Scan-201105-0116_1L
Scan-201105-0116_2R
Scan-201105-0117_1L
Scan-201105-0117_2R
Scan-201105-0118_1L
Scan-201105-0118_2R
Scan-201105-0119_1L
Scan-201105-0119_2R
Scan-201105-0120_1L
Scan-201105-0120_2R
Scan-201105-0121_1L
Scan-201105-0121_2R
Scan-201105-0122_1L
Scan-201105-0122_2R
Scan-201105-0123_1L
Scan-201105-0123_2R
Scan-201105-0124_1L
Scan-201105-0124_2R
Scan-201105-0125_1L
Scan-201105-0125_2R
Scan-201105-0126_1L
Scan-201105-0126_2R
Scan-201105-0127_1L
Scan-201105-0127_2R
Scan-201105-0128_1L
Scan-201105-0128_2R
Scan-201105-0129_1L
Scan-201105-0129_2R
Scan-201105-0130_1L
Scan-201105-0130_2R
Scan-201105-0131_1L
Scan-201105-0131_2R
Scan-201105-0132_1L
Scan-201105-0132_2R
Scan-201105-0133_1L
Scan-201105-0133_2R
Scan-201105-0134_1L
Scan-201105-0134_2R
Scan-201105-0135_1L
Scan-201105-0135_2R
Scan-201105-0136_1L
Scan-201105-0136_2R
Scan-201105-0137_1L
Scan-201105-0137_2R
Scan-201105-0138_1L
Scan-201105-0138_2R
Scan-201105-0139_1L
Scan-201105-0139_2R
Scan-201105-0140_1L
Scan-201105-0140_2R
Scan-201105-0141_1L
Scan-201105-0141_2R
Scan-201105-0142_1L
Scan-201105-0142_2R
Scan-201105-0143_1L
Scan-201105-0143_2R
Scan-201105-0144_1L
Scan-201105-0144_2R
Scan-201105-0145_1L
Scan-201105-0145_2R
Scan-201105-0146_1L
Scan-201105-0146_2R
Scan-201105-0147_1L
Scan-201105-0147_2R
Scan-201105-0148_1L
Scan-201105-0148_2R
Scan-201105-0149_1L
Scan-201105-0149_2R
Scan-201105-0150_1L
Scan-201105-0150_2R
Scan-201105-0151_1L
Scan-201105-0151_2R
Scan-201105-0152_1L
Scan-201105-0152_2R
Scan-201105-0153_1L
Scan-201105-0153_2R
Scan-201105-0154_1L
Scan-201105-0154_2R
Scan-201105-0155_1L
Scan-201105-0155_2R
Scan-201105-0156_1L
Scan-201105-0156_2R
Scan-201105-0157_1L
Scan-201105-0157_2R
Scan-201105-0158_1L
Scan-201105-0158_2R
Scan-201105-0159_1L
Scan-201105-0159_2R
Scan-201105-0160_1L
Scan-201105-0160_2R
Scan-201105-0161_1L
Scan-201105-0161_2R
Scan-201105-0162_1L
Scan-201105-0162_2R
Scan-201105-0163_1L
Scan-201105-0163_2R
Scan-201105-0164_1L
Scan-201105-0164_2R
Scan-201105-0165_1L
Scan-201105-0165_2R
Scan-201105-0166_1L
Scan-201105-0166_2R
Scan-201105-0167_1L
Scan-201105-0167_2R
Scan-201105-0168_1L
Scan-201105-0168_2R
Scan-201105-0169_1L
Scan-201105-0169_2R
Scan-201105-0170_1L
Scan-201105-0170_2R
Scan-201105-0171_1L
Scan-201105-0171_2R
Scan-201105-0172_1L
Scan-201105-0172_2R
Scan-201105-0173_1L
Scan-201105-0173_2R
Scan-201105-0174_1L
Scan-201105-0174_2R
Scan-201105-0175_1L
Scan-201105-0175_2R
Scan-201105-0176_1L
Scan-201105-0176_2R
Scan-201105-0177_1L
Scan-201105-0177_2R
Scan-201105-0178_1L
Scan-201105-0178_2R
Scan-201105-0179_1L
Scan-201105-0179_2R
Scan-201105-0180_1L
Scan-201105-0180_2R
Scan-201105-0181_1L
Scan-201105-0181_2R
Scan-201105-0182_1L
Scan-201105-0182_2R
Scan-201105-0183_1L
Scan-201105-0183_2R
Scan-201105-0184_1L
Scan-201105-0184_2R
Scan-201105-0185_1L
Scan-201105-0185_2R
Scan-201105-0186_1L
Scan-201105-0186_2R
Scan-201105-0187_1L
Scan-201105-0187_2R
Scan-201105-0188_1L
Scan-201105-0188_2R
Scan-201105-0189_1L
Scan-201105-0189_2R
Scan-201105-0190_1L
Scan-201105-0190_2R
Scan-201105-0191_1L
Scan-201105-0191_2R
Scan-201105-0192_1L
Scan-201105-0192_2R
Scan-201105-0193_1L
Scan-201105-0193_2R
Scan-201105-0194_1L
Scan-201105-0194_2R
Scan-201105-0195_1L
Scan-201105-0195_2R
Scan-201105-0196_1L
Scan-201105-0196_2R
Scan-201105-0197_1L
Scan-201105-0197_2R
Scan-201105-0198_1L
Scan-201105-0198_2R
Scan-201105-0199_1L
Scan-201105-0199_2R
Scan-201105-0200_1L
Scan-201105-0200_2R
Scan-201105-0201_1L
Scan-201105-0201_2R
Scan-201105-0202_1L
Scan-201105-0202_2R
Scan-201105-0203_1L
Scan-201105-0203_2R
Scan-201105-0204_1L
Scan-201105-0204_2R
Scan-201105-0205_1L
Scan-201105-0205_2R
Scan-201105-0206_1L
Scan-201105-0206_2R
Scan-201105-0207_1L
Scan-201105-0207_2R
Scan-201105-0208_1L
Scan-201105-0208_2R
Scan-201105-0209_1L
Scan-201105-0209_2R
Scan-201105-0210_1L
Scan-201105-0210_2R
Scan-201105-0211_1L
Scan-201105-0211_2R
Scan-201105-0212_1L
Scan-201105-0212_2R
Scan-201105-0213_1L
Scan-201105-0213_2R
Scan-201105-0214_1L
Scan-201105-0214_2R
Scan-201105-0215_1L
Scan-201105-0215_2R
Scan-201105-0216_1L
Scan-201105-0216_2R
Scan-201105-0217_1L
Scan-201105-0217_2R
Scan-201105-0218_1L
Scan-201105-0218_2R
Scan-201105-0219_1L
Scan-201105-0219_2R
Scan-201105-0220_1L
Scan-201105-0220_2R
Scan-201105-0221_1L
Scan-201105-0221_2R
Scan-201105-0222_1L
Scan-201105-0222_2R
Scan-201105-0223_1L
Scan-201105-0223_2R
Scan-201105-0224_1L
Scan-201105-0224_2R
Scan-201105-0225_1L
Scan-201105-0225_2R
Scan-201105-0226_1L
Scan-201105-0226_2R
Scan-201105-0227_1L
Scan-201105-0227_2R
Scan-201105-0228_1L
Scan-201105-0228_2R
Scan-201105-0229_1L
Scan-201105-0229_2R
Scan-201105-0230_1L
Scan-201105-0230_2R
Scan-201105-0231_1L
Scan-201105-0231_2R
Scan-201105-0232_1L
Scan-201105-0232_2R
Scan-201105-0233_1L
Scan-201105-0233_2R
Scan-201105-0234_1L
Scan-201105-0234_2R
Scan-201105-0235_1L
Scan-201105-0235_2R
Scan-201105-0236_1L
Scan-201105-0236_2R
Scan-201105-0237_1L
Scan-201105-0237_2R
Scan-201105-0238_1L
Scan-201105-0238_2R
Scan-201105-0239_1L
Scan-201105-0239_2R
Scan-201105-0240_1L
Scan-201105-0240_2R
Scan-201105-0241_1L
Scan-201105-0241_2R
Scan-201105-0242_1L
Scan-201105-0242_2R
Scan-201105-0243_1L
Scan-201105-0243_2R
Scan-201105-0244_1L
Scan-201105-0244_2R
Scan-201105-0245_1L
Scan-201105-0245_2R
Scan-201105-0246_1L
Scan-201105-0246_2R
Scan-201105-0247_1L
Scan-201105-0247_2R
Scan-201105-0248_1L
Scan-201105-0248_2R
Scan-201105-0249_1L
Scan-201105-0249_2R
Scan-201105-0250_1L
Scan-201105-0250_2R
Scan-201105-0251_1L
Scan-201105-0251_2R
Scan-201105-0252_1L
Scan-201105-0252_2R
Scan-201105-0253_1L
Scan-201105-0253_2R
Scan-201105-0254_1L
Scan-201105-0254_2R
Scan-201105-0255_1L
Scan-201105-0255_2R
Scan-201105-0256_1L
Scan-201105-0256_2R
Scan-201105-0257_1L
Scan-201105-0257_2R
Scan-201105-0258_1L
Scan-201105-0258_2R
Scan-201105-0259_1L
Scan-201105-0259_2R
Scan-201105-0260_1L
Scan-201105-0260_2R
Scan-201105-0261_1L
Scan-201105-0261_2R
Scan-201105-0262_1L
Scan-201105-0262_2R
Scan-201105-0263_1L
Scan-201105-0263_2R
Scan-201105-0264_1L
Scan-201105-0264_2R
Scan-201105-0265_1L
Scan-201105-0265_2R
Scan-201105-0266_1L
Scan-201105-0266_2R
Scan-201105-0267_1L
Scan-201105-0267_2R
Scan-201105-0268_1L
Scan-201105-0268_2R
Scan-201105-0269_1L
Scan-201105-0269_2R
Scan-201105-0270_1L
Scan-201105-0270_2R
Scan-201105-0271_1L
Scan-201105-0271_2R
Scan-201105-0272_1L
Scan-201105-0272_2R
Scan-201105-0273_1L
Scan-201105-0273_2R
Scan-201105-0274_1L
Scan-201105-0274_2R
Scan-201105-0275_1L
Scan-201105-0275_2R
Scan-201105-0276_1L
Scan-201105-0276_2R
Scan-201105-0277_1L
Scan-201105-0277_2R
Scan-201105-0278_1L
Scan-201105-0278_2R
Scan-201105-0279_1L
Scan-201105-0279_2R
Scan-201105-0280_1L
Scan-201105-0280_2R
Scan-201105-0281_1L
Scan-201105-0281_2R
Scan-201105-0282_1L
Scan-201105-0282_2R
Scan-201105-0283_1L
Scan-201105-0283_2R
Scan-201105-0284_1L
Scan-201105-0284_2R
Scan-201105-0285_1L
Scan-201105-0285_2R
Scan-201105-0286_1L
Scan-201105-0286_2R
Scan-201105-0287_1L
Scan-201105-0287_2R
Scan-201105-0288_1L
Scan-201105-0288_2R
Scan-201105-0289_1L
Scan-201105-0289_2R
Scan-201105-0290_1L
Scan-201105-0290_2R
Scan-201105-0291_1L
Scan-201105-0291_2R
Scan-201105-0292_1L
Scan-201105-0292_2R
Scan-201105-0293_1L
Scan-201105-0293_2R
Scan-201105-0294_1L
Scan-201105-0294_2R
Scan-201105-0295_1L
Scan-201105-0295_2R
Scan-201105-0296_1L
Scan-201105-0296_2R
Scan-201105-0297_1L
Scan-201105-0297_2R
Scan-201105-0298_1L
Scan-201105-0298_2R
Scan-201105-0299_1L
Scan-201105-0299_2R
Scan-201105-0300_1L
Scan-201105-0300_2R
Scan-201105-0301_1L
Scan-201105-0301_2R
Scan-201105-0302_1L
Scan-201105-0302_2R
Scan-201105-0303_1L
Scan-201105-0303_2R
Scan-201105-0304_1L
Scan-201105-0304_2R
Scan-201105-0305_1L
Scan-201105-0305_2R
Scan-201105-0306_1L
Scan-201105-0306_2R
Scan-201105-0307_1L
Scan-201105-0307_2R
Scan-201105-0308_1L
Scan-201105-0308_2R
Scan-201105-0309_1L
Scan-201105-0309_2R
Scan-201105-0310_1L
Scan-201105-0310_2R
Scan-201105-0311_1L
Scan-201105-0311_2R
Scan-201105-0312_1L
Scan-201105-0312_2R
Scan-201105-0313_1L
Scan-201105-0313_2R
Scan-201105-0314_1L
Scan-201105-0314_2R
Scan-201105-0315_1L
Scan-201105-0315_2R
Scan-201105-0316_1L
Scan-201105-0316_2R
Scan-201105-0317_1L
Scan-201105-0317_2R
Scan-201105-0318_1L
Scan-201105-0318_2R
Scan-201105-0319_1L
Scan-201105-0319_2R
Scan-201105-0320_1L
Scan-201105-0320_2R
Scan-201105-0321_1L
Scan-201105-0321_2R
Scan-201105-0322_1L
Scan-201105-0322_2R
Scan-201105-0323_1L
Scan-201105-0323_2R
Scan-201105-0324_1L
Scan-201105-0324_2R
Scan-201105-0325_1L
Scan-201105-0325_2R
Scan-201105-0326_1L
Scan-201105-0326_2R
Scan-201105-0327_1L
Scan-201105-0327_2R
Scan-201105-0328_1L
Scan-201105-0328_2R
Scan-201105-0329_1L
Scan-201105-0329_2R
Scan-201105-0330_1L
Scan-201105-0330_2R
Scan-201105-0331_1L
Scan-201105-0331_2R
Scan-201105-0332_1L
Scan-201105-0332_2R
Scan-201105-0333_1L
Scan-201105-0333_2R
Scan-201105-0334_1L
Scan-201105-0334_2R
Scan-201105-0335_1L
Scan-201105-0335_2R
Scan-201105-0336_1L

Citation preview

c Didattica e programmazione

© 2(K)4 Pearsnn Paravia Hruno Mondadori S.p.A.

Authoriud tnmsbllion from thr J::nglùh /,mguage t•dition, emitkd· A Book 011 C: proRrammillR in C- Fourth Edition hy kelley, Al; Pohl, Ira, puhlished hy Pear.\'011 Educatimi, /ne, puhlishing as Addi.wm We.\·/ey Prt~/i~ssional, Copyright~) l 99X. p11rf of this book 11/tlJ br reproducrd or tmmmillt'd in ,u~y fòrm or ~Y ,u~y meaw, rlt·ctronic or mechtmic,d, including photocopying, remrding or f,y tlllJ injòrnltltion slmìlY,t' rrtrirval ~)'.ffl'm, without pamission from Pe~~rson T:ductlfÙm, /ne.

Ali rights rt·)·erved. No

/t,tfitm languagr ulition published ~y Pt'ar.wm Parada Bmno Mondculori S.p.A., Copyright © 2004 Le inf{>rmazioni coruenute in quesro libro sono state vcritìcarc c docunH.'IH= 0; --i) putchar(name[i]); printf("\n%s%d%s\n\n%s\n", "and the letters in your name sum to ", sum, "Have anice day!"); return 0;

. ,

}

Eseguendo il programma e inserendo, quando appare il prompt, il nome Alice B. Carole, si ottengono sullo schermo le seguenti righe: Hil

What is your name?

Alice B. Carole

Nice to meet you Alice B. Carole. Your name spelled backwards is eloraC .B ecilA and the letters in your name sum to 1142. Have a nice day!

ANALISI DEL PROGRAMMA nice_day •

#include #include Il file standard di intestazione stdio.h contiene il prototipo della funzione printf (). Esso contiene inoltre le definizioni delle macro getchar () e putchar (),utilizzate nel programma, rispettivamente, per leggere caratteri dalla tastiera e scrivere caratteri sullo schermo. nfile standard di intestazione ctype.h contiene la definizione della macro isalpha (),utilizzata per determinare se un carattere sia alfabetico, cioè se sia una lettera maiuscola o minuscola.

36

Capitolo 1



#define MAXSTRING 100 La costante simbolica MAXSTRING viene utilizzata per fissare la dimensione del vettore name. n programma è stato scritto supponendo che l'utente non inserisca più di 99 caratteri. Perché 99 caratteri? Perché il sistema aggiungerà ' \0\ ' come carattere marcatore che termina la stringa.



char c, name[MAXSTRING]; int i, sum = 0; La variabile c è di tipo char. L'identificatore name è di tipo "array di char" e ha dimensione MAXSTR ING. In C gli indici degli array cominciano da O, dunque gli elementi di questo array sono name[0] ,name[ 1], ... ,name[MAXSTRING - 1 ]. Le variabili i e sum sono di tipo int; sum è inizializzata a O.



printf("\nHil What is your name? "); Questo messaggio chiede all'utente di inserire i dati: il programma attende l'inserimento di un nome seguito dal carattere newline (Invio o Return sulla tastiera).



(c = getchar ()) 1= '\n' Questa espressione è formata da due parti. A sinistra appare: (c = getchar ())

A differenza di altri linguaggi, in C l'assegnamento è un operatore (vedi Paragrafo 2.11). In questo caso, getchar() viene utilizzata per leggere un carattere dalla tastiera e per assegnarlo a c. Il valore complessivo dell'espressione corrisponde a qualsiasi valore assegnato a c. Le parentesi sono necessarie in quanto la priorità dell'operatore =è minore di quella dell'operatore 1=. Dunque c = getchar () l= ' \n' è equivalente a c= (getchar() l= '\n') che è sintatticamente corretto, ma differente da quanto desiderato. Nel Paragrafo 2.9 verranno analizzati approfonditamente i concetti di precedenza e associativà degli operatori. •

for (i= 0; (c= getchar()) l= '\n'; ++i) { name[i] = c; if (isalpha(c)) sum += c; }

Alla variabile i viene assegnato inizialmente il valore O. Quindi getchar () riceve un carattere dalla tastiera, lo assegna a c e controlla se si tratta di un carattere newline; in caso negativo l'esecuzione prosegue con il corpo del ciclo for.

Panoramica sul C

37

Per prima cosa, il valore di c viene assegnato all'elemento name [i] dell'array; poi, utilizzando la macro isalpha (),viene determinato se c sia una lettera (minuscola o maiuscola): in caso affermativo sum viene incrementata del valore di c. Come mostrato nel Paragrafo 3.3, in C un carattere ha un valore intero corrispondente alla sua codifica ASCII. Per esempio, a ha valore 97, b ha valore 98 e così via. Alla fine dell'esecuzione del corpo del ciclo for la variabile i viene incrementata. Il ciclo for viene eseguito ripetutamente fino al momento in cui viene ricevuto un carattere newline. l



l

l

l

name [i] = \0 Dopo la fine del ciclo for, a name [i] viene assegnato il carattere nullo, denotato con i simboli \0. Per convenzione, ogni stringa termina con un carattere nullo; le funzioni che elaborano stringhe, come printf (),utilizzano il carattere nullo come marcatore, o sentinella, di fine stringa. È possibile pensare alla rappresentazione in memoria dell'array name nel modo illustrato di seguito. l

l

;

l A Il l i l c l e l l B l . l l C l a l r l o Il l e l\ol * l O

l

2

3

4

S

6

7

8

9

IO

Il

12 13

14 IS

16

~ 99



printf("\n%s%s%s\n%s", "N ice t o me et you ", name, " . " , "Your name spelled backwards is "); Si osservi che per stampare l'array di caratteri name viene utilizzato il formato %s. Gli elementi dell'array vengono stampati uno dopo l'altro fino a raggiungere il marcatore di fine stringa \0.



for (--i; i >= 0; --i) putchar(name[i]); Supponendo che sia stata digitata come input la sequenza di caratteri Alice B. Carole seguita da un carattere newline, il valore della variabile i all'inizio di questo ciclo for è 15 (non si dimentichi che il conteggio avviene da O e non da l). Dopo che i è stata decrementata, il suo valore corrisponde all'indice dell'ultimo carattere del nome inserito in input Dunque, l'effetto di questo ciclo for è quello di stampare tale nome al contrario sullo schermo.



printf("\n%s%d%s\n\n%s\n", "and the letters in your name sum to • sum, ".", "Have anice dayl"); Viene stampata la somma delle lettere che costituiscono il nome inserito in input, seguita da un messaggio finale. Dopo la stampa della somma delle lettere del nome, viene stampato un punto. Per creare uno spazio bianco prima della stampa del messaggio finale vengono usati due nuovi caratteri newline. Si noti che lo stile di questa printf () consente di visualizzare facilmente ciò che appare sullo schermo.

38

Capitolo

1

Puntatori Un puntatore è l'indirizzo di un oggetto in memoria. Poiché anche i nomi di array sono puntatori, l'utilizzo di array e l'utilizzo di puntatori sono strettamente legati. Nel seguente programma sono illustrate alcune di queste relazioni:

Nel file abc.c: #include #include #define MAXSTRING in t ma in (v o id)

100

{

char

c= 'a', *p, s[MAXSTRING];

P = &c;

printf("%c%c%c ", *p, *p+ 1, *p+ 2); strcpy ( s, "ABC"); printf("%s %c%c%s\n", s, *s + 6, *s + 7, s + 1); strcpy(s, "she sells sea shells by the seashore");

p

=s

+ 14;

for (;*p l= '\0'; ++p) { if (*p== 'e') *p = , E,; i f (*p == l l) *p= '\n'; }

printf("%s\n", s); return 0; }

L'output del programma è: abc ABC GHBC she sells sea shElls by thE sEashorE

ANALISI DEL PROGRAMMA abc •

#include La libreria standard contiene svariate funzioni per la gestione delle stringhe (vedi Paragrafo 6.11); nel file d'intestazione string.h sono contenuti i prototipi di tali funzioni. In questo programma viene utilizzata st rcpy ( ) per copiare una stringa.



char c= 'a', *p, s[MAXSTRING]; La variabile c è di tipo char; essa viene inizializzata con il valore ' a' . La variabile p è di tipo puntatore a char. La stringa s ha dimensione MAXSTRING.

Panoramica sul C

39



P = &c; Il simbolo & è l'operatore di indirizzo; il valore dell'espressione &c è l'indirizzo di memoria della variabile c. L'indirizzo di c viene assegnato a p: si può dunque pensare che p .. punti a c".



printf( "%c%c%c M, *p, *p + 1, *p + 2); Il formato %c viene utilizzato per stampare come carattere il valore di un'espressione. Il simbolo * costituisce l'operatore di dereferenziazione, o indirizzamento indiretto. L'espressione *p assume il valore di ciò a cui punta p; poiché p punta a c e c ha valore a a è il valore dell'espressione *p; dunque viene stampato il carattere a. L'espressione *p + 1 ha come valore uno più il valore dell'espressione *p; perciò viene stampato il carattere b. Infine, l'espressione *p + 2 ha come valore 2 più il valore dell'espressione *p; perciò viene stampato il carattere c. l



l,

l

l

"ABC"

Una costante stringa viene memorizzata sotto forma di vettore di caratteri, l'ultimo dei quali è il carattere nullo \0. Pertanto la dimensione della costante stringa "ABC" è 4 e non 3. Anche la stringa vuota " • contiene un carattere, cioè \0. È importante comprendere che le costanti stringa sono di tipo ..array di char". Un nome di array a sé stante viene trattato come un puntatore, e ciò vale anche nel caso di una costante di tipo stringa. •

strcpy(s, "ABC"); La funzione strcpy() riceve due parametri, entrambi di tipo puntatore a char, che possono essere visti come stringhe. La stringa a cui punta il secondo parametro viene copiata in memoria a partire dalla locazione a cui punta il primo parametro. Tutti i caratteri fino al carattere nullo compreso vengono ricopiati, con il risultato di copiare una stringa in un'altra. Il programmatore deve far sì che vi sia spazio sufficiente, a partire dalla locazione a cui punta il primo parametro, per memorizzare tutti i caratteri che devono essere copiati. La memoria utilizzata da s può essere schematizzata, dopo l'esecuzione di questa istruzione, come illustrato nella figura seguente.

l A l B l C l\ol * l · · · ~ o



l

2

3

4

99

p r i nt f ( "%s %c%c%s \ n " , s , *s + 6 , *s + 7 , s + 1 ) ; Il nome del vettore s, da solo, è un puntatore. Si può pensare che s punti a s [ 0] o, in altre parole, che s sia l'indirizzo base dell'array, cioè l'indirizzo di s [ 0]. Stampando s come stringa viene stampato ABC. L'espressione *s ha il valore di ciò a cui punta s, cioè di s [ 0], quindi il valore di questa espressione è A. Poiché la sesta lettera successiva ad A è G e la settima è H, le espressioni *s + 6 e *s + 7 stampate con il formato di un carattere produrranno in output rispettivamente i caratteri G e H. L'espressione s + 1 è un esempio di aritmetica dei puntatori; il suo valore è un puntatore che punta a s [ 1], il carattere successivo nell'array. Dunque la stampa sotto forma di stringa di s + 1 produce in output BC.

40

Capitolo 1



strcpy ( s, "she sells se a shells by the seashore"); Una nuova stringa viene copiata in s; il valore precedentemente memorizzato in s viene sovrascritto.



p = s + 14; Viene assegnato a p il valore del puntatore s + 14. Un'istruzione equivalente è: p = &s[ 14]; Contando accuratamente si può osservare che ora p punta alla prima lettera della parola "shells". È opportuno notare che sebbene s sia un puntatore esso non è una variabile, ma un puntatore costante. Un'istruzione come

P

=

s;

è consentita in quanto p è una variabile puntatore, mentre l'istruzione

s

= p;

provocherebbe un errore sintattico. Nonostante il valore a cui spunta possa essere modificato, non è possibile modificare il valore di s stesso. •

for (;*p l= '\0'; ++p) { if (*p== 'e') *p = , E,; if (*p== l ' ) *p= '\n'; }

Il corpo del ciclo for viene eseguito fintanto che il valore a cui punta p è diverso dal carattere nullo. Se il valore a cui punta p è uguale a ' e ' , allora il valore in memoria viene modificato in E se il valore a cui punta p è uguale allo spazio allora il valore in memoria è sostituito con \n Alla fine del corpo del ciclo f or, la variabile p viene incrementata; in questo modo p va a puntare al carattere successivo della stringa. l

l ;

l

l



l ,

l •

printf("%s\n", s);

La variabile s viene stampata con il formato di stringa, seguita da un carattere newline. Poiché il ciclo for precedente ha modificato i valori di alcuni elementi dell'array, l'output prodotto è il seguente. she sells sea shElls by

thE sEashorE

Panoramica sul C

41

In C, array, stringhe e puntatori sono strettamente legati. Si consideri per esempio la dichiarazione: char

*p, s[100];

Essa crea l'identificatore p di tipo puntatore a char e l'identificatore s di tipo vettore di 100 elementi di tipo char. Poiché il nome di un array è un puntatore, sia p che s risultano puntatori a char. Tuttavia, mentre p è una variabile, s è una costante che punta a s [ 0]. Si osservi che l'espressione ++p può essere utilizzata per incrementare p, ma d'altra parte, essendo s una costante, l'espressione ++s è scorretta: il valore di s non può essere modificato. È fondamentale il fatto che le due espressioni s[i]

e

*(S +

i)

siano equivalenti. L'espressione s [i 1 ha come valore l'i-esimo (partendo da O) elemento dell'array, mentre * ( s + i) è l'indirizzamento indiretto di s + i, espressione con puntatori che punta i caratteri dopo s. Analogamente, le due espressioni p[i]

*(p + i)

e

risultano equivalenti.

1.9

File

L'utilizzo dei file in C è semplice; il seguente codice può essere utilizzato per aprire un file di nome my_file:

Nel file read_it.c: #include int main(void) {

in t FILE

c·,

*ifp;

ifp = fopen( "my_file", "r"); Nella seconda riga nel corpo di main () viene dichiarato un puntatore a FILE di nome i fp (abbreviazione di "infile pointer"). La funzione fopen () si trova nella libreria standard, e il suo prototipo in stdio.h. Il tipo FILE è definito, come struttura particolare, in stdio.h. Questo costrutto può essere utilizzato senza conoscerne i dettagli; tuttavia, prima di qualunque riferimento a FILE è necessario avere incluso il file d'intestazione mediante la direttiva #include. La funzione fopen () riceve come parametri due strin-

42

Capitolo 1

ghe e restituisce un puntatore a FILE; il primo argomento è il nome del file, il secondo indica la modalità di apertura del file stesso. Modalità di apertura del file "r" "w" "a''

per la lettura (read) per la scrittura (write) per l'aggiunta (append)

L'apertura per la scrittura di un file non esistente determina la creazione di un nuovo file, mentre se tale file è esistente il suo contenuto viene distrutto e la scrittura ha luogo a partire dall'inizio del file stesso. Se per qualche ragione il file non risulta accessibile, fopen () restituisce il puntatore NULL. Un file, dopo essere stato aperto, può essere individuato per mezzo del relativo puntatore. Al termine dell'esecuzione di un programma il sistema C chiude automaticamente tutti i file aperti; ciascun sistema C prevede un limite, generalmente di 20 o 64, al numero di file aperti simultaneamente. Nel caso vengano utilizzati parecchi file, il programmatore dovrebbe chiudere esplicitamente tutti i file non correntemente in uso: a tale scopo è disponibile la funzione di libreria fclose (). Esaminiamo ora l'utilizzo dei file. Dato un testo, è facile analizzare il numero di occorrenze delle lettere e delle parole che lo compongono; analisi di questo tipo si sono dimostrate utili in svariate discipline, dallo studio dei geroglifici a quello di Shakespeare. Per semplicità, il programma presentato nel seguito si limita a contare le occorrenze delle lettere maiuscole. Per esemplificare l'impiego del programma viene utilizzato un file di nome chapterl, contenente la versione inglese di questo capitolo. Il programma, di nome cnt_letters, effettua l'analisi di tale testo utilizzando file in lettura e scrittura. L'operazione ha inizio fornendo il comando

cnt_letters chapter1 data1 dove chapter l e data l sono i parametri per il programma. La presentazione è preceduta da una breve discussione sull'accesso dall'interno di un programma ai parametri fomiti su una riga di comando. Il linguaggio C fornisce un collegamento con i parametri presenti sulla riga nella quale viene impartito il comando; tale collegamento può essere utilizzato mediante il seguente codice: #include int main(int argc, char •argv[]) {

Fino a questo punto la funzione mai n ( ) è stata utilizzata senza parametri; in realtà essa può assumere un numero variabile di parametri. Il parametro argc ("argument count") assume come valore il numero di argomenti presenti sulla riga con la quale è stato

Panoramica sul C

43

impartito il comando di esecuzione del programma. Il parametro argv ("argument variable") è un array di puntatori a char, che può essere pensato come un array di strint(he; i suoi elementi puntano, secondo l'ordine, alle parole presenti sulla riga di comando impiegata per eseguire il programma, pertanto argv [ 0] è un puntatore al nome Ktesso del comando. Come esempio di utilizzo di questa possibilità, si supponga di aven· scritto il programma e di avere posto il relativo codice eseguibile nel file cnt_letters. Lo scopo della riga di comando

cnt_letters chapter1 data1

i· quello di richiamare il programma cnt_letters con i due nomi di file chapterl e datai rome argomenti. Il programma dovrebbe leggere il file chapterl e scrivere nel file datai. Fornendo un comando differente, come

cnt_letters chapter2 data2 il programma dovrebbe leggere il file chapter2 e scrivere nel file data2. Le tre parole presenti sulla riga di comando risultano accessibili al programma mediante i tre puntatori argv[0], argv[ 1] e argv[2]. Di seguito è riportato il testo del programma.

Nel file cnt_letters.c:

l* Conta le lettere maiuscole in un file. *l #include #include int main(int argc, char •argv[]) {

int FILE

c, i, letter[26]; *ifp, *ofp;

if (argc l= 3) { printf("\n%s%s%s\n\n%s\n%s\n\n", "Usage: ", argv[0], " infile outfile", "The uppercase letters in infile will be counted.", "The results will be written in outfile."); exit(1); }

ifp = fopen(argv[1], "r"); ofp = fopen ( argv [ 2] , "w"); far (i= 0; i< 26; ++i)

l* inizializza a zero *l

1 array 1

letter[i] = 0; while ((c = getc(ifp)) != EOF) i f (c >= A && c

=

da sinistra a destra

l=

da sinistra a destra

&& Il

da sinistra a destra

?:

da destra a sinistra

da sinistra a destra +=

*=

/=

ecc.

da destra a sinistra da sinistra a destra

, (operatore virgola)

L'operatore l è unario; tutti gli altri operatori relazionali, di uguaglianza e logici sono binari. Essi operano su espressioni e restituiscono un valore di tipo int che può essere O o l. Infatti, nel linguaggio C falso è rappresentato da un valore nullo, mentre vero è rappresentato da qualunque valore diverso da zero; più precisamente, qualunque valore nullo, come O, 0.0, il carattere nullo \0 o il valore puntatore NULL, rappresenta il valore falso, mentre ogni valore non nulle rappresenta vero. lntuitivamente, un'espressione come a < b può essere sia vera che falsa; nel primo caso il suo valore in C è l, altrimenti è O; in entrambi casi il valore è di tipo int. l

l

Espressioni e operatori relazionali

4.2

Gli operatori relazionali


=

sono tutti binari. Essi hanno come operandi due espressioni e restituiscono un risultato di tipo int che può essere O o l.

relational_expression

::-=

expr < expr expr > expr expr = expr

Flusso del

controllo

133

Alcuni esempi sono:

a a


b

-1.3 >= (2.0 * x a < b < c

+

3.3)

l* corretto sintatticamente, ma confuso *l

ma non:

a

=< b < = b

a

>>

a

l* non esiste *l l* non è permesso lo spazio *l l* è un'espressione di shift *l

b

Si consideri un'espressione relazionale come a < b. Se a è minore di b il valore dell'espressione, di tipo in t, è l, che è da considerarsi vero. Se a non è minore di b il valore dell'espressione, di tipo in t, è O, che è da considerasi falso. Si noti che il valore dell'espressione a < b è lo stesso di a - b < 0. Essendo la priorità degli operatori relazionali minore di quella degli operatori aritmetici, l'espressione

a - b

< 0

è equivalente a:

(a -

b) < 0

Su molte macchine, un'espressione come a < b viene implementata mediante l'espres-sione equivalente a - b < 0. Nella valutazione di espressioni relazionali si applicano le conversioni aritmetiche usuali. Siano el e e2 due espressioni aritmetiche arbitrarie. Nella seguente tabella viene mostrato come il valore di el - e2 determini i valori delle espressioni relazionali. Valori di: e1- e2

positivo zero negativo

e1 < e2

e1 > e2

e1 = e2

o o

o

l

o

o

l l

l l

l

o

La tabella della pagina seguente presenta l'utilizzo delle regole di priorità e associatività per la valutazione di espressioni relazionali. Due espressioni riportate in essa forniscono risultati sorprendenti in quanto non conformi alle usuali regole matematiche. In matematica, si scrive

3 0);

Flusso del controllo 153

Si consideri un costrutto della forma: do

statement

while (expr) ;

next statement Prima di tutto viene eseguita statement, poi viene valutata expr. Se il valore di expr è diverso da zero (vero), il controllo torna all'inizio dell'istruzione do ripetendo il medesimo processo. Quando expr è zero (falso), il controllo passa a next statement. Come esempio si supponga di voler leggere da input un intero positivo, ripetendo la lettura finché il dato inserito non sia veramente un intero positivo. A tale scopo è possibile utilizzare il seguente codice: do { printf("Input a positive integer: "); scanf( "%d", &n); if (error = (n x- y); 9.

Dite che cosa viene stampato e fomite una spiegazione.

int

i = 7,

j

= 7;

i f (i == 1) if (j == 2)

printf("\d\n", i= i+ j); else printf( "%d\n•, i = i - j); printf("%d\n•, i); 10. Scrivete un programma di prova contenente questa porzione di codice per scoprire come il vostro compilatore segnala l'errore di sintassi. Spiegate il perché.

while (++i < LIMIT) do { j = 2 * i + 3; printf("j = %d\n•, j);

l* errore di sintassi */

}

l* Molti altri linguaggi richiedono "do", ma non il C. */

11.

Dite se il seguente codice può causare cicli infiniti. Motivate la risposta, assumendo che i valori di i e j non vengano mai modificati all'interno del ciclo.

166

Capitolo 4

printf ( "Input two integers: "); scanf("%d%d", &i, &j); while (i * j < 0 && ++i l= 7 && j++ l= 9) { /* qualcosa da eseguire */ }

12.

Come viene spiegato nel Paragrafo 4.8, se il valore di n è negativo while (--n) l* qualcosa da eseguire */

è un ciclo infinito. In realtà questo non è vero. Siete in grado di spiegame il motivo? 13. Scrivete un programma che legga un valore intero n e sommi gli interi da n a 2 * n se n non è negativo, e da 2 * n a n altrimenti. Scrivete il codice in due versioni: una che utilizzi esclusivamente cicli for, l'altra che utilizzi esclusivamente cicli while. 14.

La funzione putchar () restituisce il valore intero del carattere stampato in output Quale esito ha l'esecuzione del frammento di codice che segue? for (putchar( l 1 1); putchar( l 2 l); putchar( 13 l)) putchar( 4 1

1

);

15.

Per il linguaggio C, stabilite quali delle seguenti affermazioni sono vere e quali false. Ogni istruzione termina con un punto e virgola. Ogni istruzione contiene almeno un punto e virgola. Ogni istruzione contiene al massimo un punto e virgola. Esiste un'istruzione contenente esattamente 33 punti e virgola. Esiste un'istruzione di 35 caratteri che contiene 33 punti e virgola.

16.

Nel Paragrafo 4.10 viene presentato un programma che stampa una tabella di valori di alcune funzioni booleane. Eseguite tale programma ed esaminatene l'output. Tra i 32 input differenti, esattamente la metà, cioè 16, hanno il valore di majority uguale a l. Scrivete un programma che stampi una tabella dei valori della funzione maj ori ty per 7 variabili booleane. Quanti dei 128 input differenti hanno il valore di majority uguale a l? Stabilite ciò che avviene nel caso generale in forma di teorema tentando di dame una dimostrazione Oa vostra macchina può esservi d'aiuto nel trovare teoremi compiendo verifiche su casi particolari, ma in generale non può fornirvi le dimostrazioni).

Flusso del controllo 167

17.

Scrivete tre versioni di un programma che calcoli la somma dei primi n numeri pari e dei primi n dispari. Il valore di n dovrebbe essere inserito interattivamente. Per la prima versione utilizzate il codice: for (cnt = 0, i= 1, j = 2; cnt b1 Il b2 Il b3 Il b4 (b) b1 && b2 && b3 && b4 ( c > 1 ( 1b 1 l l b2 > && ( 1b3 Il b4 >

170

Capitolo 4

Utilizzate le lettere V e F per rappresentare rispettivamente i valori vero e falso. Suggerimento: utilizzate la direttiva #def i ne per definire un'espressione BOOLEX e scrivete il programma in modo che possa operare su un'espressione BOOLEX arbitraria. 26.

Scrivete un programma per controllare l'impiego appropriato delle parentesi graffe. n programma dovrebbe avere due variabili: una variabile di nome left_cnt che conti il numero di parentesi aperte e un'altra di nome right_cnt che conti il numero di quelle chiuse. Entrambe le variabili devono essere inizializzate a zero. Il programma dovrebbe leggere e stampare ogni carattere del file di input incrementando, quando viene incontrata una parentesi, il contatore opportuno. Se right_cnt supera left_cnt il programma scritto dovrebbe inserire una coppia di caratteri ?? nel punto corrispondente in output. Quando tutti i caratteri in input sono stati elaborati, le due variabili left_cnt e right_cnt dovrebbero avere lo stesso valore. In caso contrario, se left_cnt è maggiore di right_cnt dovrebbe venire stampato un messaggio che indichi il numero di parentesi mancanti, stampando una serie di }. Per esempio: ERRORE: Parentesi chiuse mancanti: }}}

Per l'input/ouput utilizzate le macro getchar () e putchar ().Provate il programma utilizzando alcuni file contenenti del codice C scritto da voi.

27. Estendete il programma dell'esercizio precedente in modo da trattare, oltre che le parentesi graffe, anche le tonde. 28.

Nel Paragrafo 4.8 viene presentato un programma che conta gli spazi, le cifre e le lettere. Modificate il programma in modo che conti separatamente le lettere minuscole e le lettere maiuscole.

29.

Riscrivete le due porzioni di codice seguente senza utilizzare break e continue. while (c if

(c

= getchar()) == 'E' )

{

break; ++cnt; if (c>= '0' && c= 0.0) return x; else return -x; }

Un programma non deve necessariamente utilizzare il valore eventualmente restituito da una funzione. while ( ..... ) { getchar(); c= getchar();

l* legge un carattere, ma non lo utilizza *l l* c sarà utilizzato *l

}

5.3

Prototipi di funzione

Le funzioni dovrebbero venire dichiarate prima di essere utilizzate. ANSI C fornisce

una nuova sintassi per la dichiarazione di funzioni detta prototipo di funzione, che informa il compilatore sul numero e il tipo dei parametri che devono essere passati alla funzione e sul tipo del valore da essa restituito. Un esempio è il seguente: double

sqrt(double);

Il prototipo nell'esempio segnala al compilatore che sqrt () è una funzione che riceve un unico parametro di tipo double e restituisce un valore di tipo double. La forma generale di un prototipo di funzione è

type function_name ( parameter type list ) ; dove parameter type list (elenco dei tipi dei parametri) è tipicamente un elenco di tipi separati da virgole. Gli identificatori sono opzionali e non influiscono sul prototipo. Per esempio, il prototipo di funzione void f(char c, int i);

è equivalente a: void f(char, int);

180 Capitolo 5

Gli identificatori come c e i nell'elenco dei tipi dei parametri nei prototipi di funzione non sono utilizzati dal compilatore; essi sono utili come documentazione per il programmatore e per chi legge il codice. Se la funzione è priva di argomenti si utilizza la parola chiave void; essa viene utilizzata anche per specificare che la funzione non restituisce alcun valore. Per specificare che una funzione può ricevere un numero di parametri variabile si utilizza un'ellissi (. .. ).Come esempio si veda il prototipo di funzione di printf () nel file d'intestazione standard stdio.h. I prototipi di funzione permettono al compilatore di verificare il codice in modo più approfondito, e inoltre i valori passati alla funzione vengono, quando possibile, convertiti opportunamente. Per esempio, se è stato specificato il prototipo di sqrt (),la chiamata sq rt ( 4) produrrà il valore corretto: poiché il compilatore sa che il parametro di sq rt ( ) è di tipo double, il valore 4 di tipo int sarà promosso a double e pertanto verrà restituito il valore corretto (per un'ulteriore discussione vedi Esercizio 5). Nel C tradizionale non sono permessi elenchi dei tipi dei parametri nelle dichiarazioni di funzioni. Per esempio, la dichiarazione di funzione di sq rt ( ) è data da: double

sqrt ();

l* stile del C tradizionale */

Sebbene i compilatori ANSI C accettino anche questo stile, l'utilizzo dei prototipi di funzione è preferibile. Con questa dichiarazione la chiamata di funzione sqrt ( 4) non produce il risultato corretto (vedi Esercizio 5).

Prototipi di funzione In C++ In C++ i prototipi di funzione sono obbligatori, mentre l'impiego della parola void nell'elenco dei tipi dei parametri è facoltativo sia nei prototipi che nelle definizioni di funzione. Pertanto, in C++ void f ()

è equivalente a: void f(void) Si osservi con attenzione che ciò è in conflitto con il C tradizionale, dove una dichiarazione di funzione come in t f (); specifica chef () riceve un numero di parametri non precisato. Nel C tradizionale void non è una parola chiave; dunque essa non può essere utilizzata nell'elenco dei parametri di una dichiarazione o di una definizione di funzione.

Funzioni

5.4

181

Un esempio: costruzione di una tabella di potenze

In questo paragrafo viene fornito un esempio di programma composto da un certo numero di funzioni, che per semplicità vengono scritte una dopo l'altra in un unico file. Lo scopo del programma è quello di stampare una tabella di potenze.

#include Ndefine long void void

N

7

power(int, int); prn_heading(void); prn_tbl_of_powers(int);

int main(void) {

prn_heading(); prn_tbl_of_powers(N); return 0; }

void prn_heading(void) {

printf("\n:::::

A TABLE OF POWERS

:::: :\n\n");

}

void prn tbl of powers(int n)

-

{

int

-

-

i, j ;

for (i= 1; i O

o, in modo equivalente, da: O!= l,

n! • n((n- l)!)

per n> O

Dunque, per esempio, 5! • 5 · 4 · 3 · 2 · l = 120. Basandosi sulla definizione ricorsiva del fattoriale, è facile scrivere una versione ricorsiva della funzione fattoriale.

int factorial(int n)

/* versione ricorsiva */

{

if (n 1; --n) product *= n; return product; }

Per lo stesso valore di input, entrambe le funzioni fattoriali restituiscono il medesimo risultato, ma la versione iterativa richiede solo una chiamata di funzione indipendentemente dal valore del parametro che le viene passato. Nell'esempio successivo viene mostrata una funzione ricorsiva che manipola caratteri. Essa può essere facilmente riscritta mediante una funzione iterativa equivalente, e tale compito viene lasciato come esercizio per il lettore. n programma invita prima di tutto l'utente a inserire una riga di testo, poi, per mezzo di una funzione ricorsiva, la stampa al contrario. l* Stampa una riga al contrario. *l

#include void

wrt_it(void);

int main(void) {

printf ( • Input a line: wrt i t (); printf("\n\n"); return 0;

");

}

void wrt it(void) { int c; if ((c= getchar()) l= '\n') wrt_it ();

202

Capitolo 5

putchar(c); }

Ecco cosa compare sullo schermo se l'utente inserisce la riga sing a song of sixpence: Input a line: sing a song of sixpence ecnepxis fo gnos a gnis

ANALISI DEL PROGRAMMA wrt_bkwrds •

printf("Input a line: "); wrt_it (); Viene prima di tutto stampato un prompt per l'utente. L'utente scrive una riga e la conclude con un a capo, cioè il carattere \n. Viene quindi chiamata la funzione ricorsiva wrt_i t ().



void wrt_it(void) {

int c; Sarebbe stato possibile dichiarare c di tipo char, in quanto questa funzione non effettua test su EOF. •

if ((c= getchar()) l= '\n') wrt_it(); Se il carattere letto non è un newline, viene chiamata di nuovo wrt_i t ( ) , che legge un altro carattere, e così via. Ogni chiamata utilizza un proprio spazio locale di memoria per la variabile c. Ogni variabile c è locale alla particolare chiamata della funzione wrt_i t () e contiene uno dei caratteri dell'input stream. Le chiamate di funzione vengono accumulate fino a quando viene letto il carattere newline.



putchar(c); La scrittura in output avviene solo dopo la lettura del carattere newline. Ogni chiamata di wrt_i t () stampa il valore memorizzato nella propria variabile locale c. Per primo viene stampato il carattere newline, poi il carattere precedente e così via, sino al primo carattere. Pertanto la riga in input viene rovesciata.

Considerazioni sull'efficienza Molti algoritmi hanno formulazioni sia iterative che ricorsive. In generale la ricorsione risulta più elegante e richiede meno variabili per effettuare lo stesso calcolo. La ricorsione si incarica di mantenere i valori intermedi accumulando su una struttura a pila (stack) parametri e variabili relativi a ciascuna chiamata. Questo tipo di memorizzazione, invisibile all'utente, è dispendioso in termini di spazio e di tempo: su alcune macchine una

Funzioni 203

semplice chiamata ricorsiva con un parametro intero può richiedere l'impiego di 8 parole da 32 bit sullo stack. Viene ora discussa l'efficienza del calcolo della sequenza di Fibonacci, un esempio particolarmente rilevante. Questa sequenza è definita ricorsivamente da:

lo- O, ft • l, 1;. 1 - f; + f;_ 1 per i- l, 2, ... Eccettuati lo e ft, ogni elemento della sequenza è la somma dei due elementi precedenti. I primi elementi della sequenza sono O, l, l, 2, 3, 5, .... Ecco una funzione che calcola ricorsivamente i numeri di Fibonacci:

int fibonacci(int n) {

if (n= N) error_exit_too_many_words(); if (strlen(word) >= MAXWORD) error_exit_word_too_long(); w[i] = calloc(strlen(word) + 1, sizeof(char)); if (w[i] == NULL) error exit calloc failed(); strcpy(w(i], word); } n

i;

sort words(w, n); wrt words(w, n); return 0;

l* ordina le parole */ l* stampa le parole ordinate */

}

ANALISI DI main() NEL PROGRAMMA sort_words •

#define MAXWORD 50 l* massima lunghezza di una parola */ #define N 300 l* lunghezza di w[] */ Queste due righe del file d'intestazione definiscono le costanti simboliche MAXWORD e N, usate in mai n () e in altre funzioni. Assumiamo che nel file vi siano al massimo 300 parole e che nessuna di esse sia più lunga di 50 caratteri.



char word[MAXWORD]; l* spazio di lavoro*/ char *w[N]; l* array di puntatori */ Useremo l'array word per memorizzare temporaneamente ogni parola letta dal file di input, e ci aspettiamo che ogni parola abbia meno di MAXWORD caratteri. Ricordiamo che la dichiarazione char

*w[ N];

è equivalente a:

char

*(w[N]);

Quindi wè un array di puntatori a char che alla fine conterrà tutte le parole presenti nel file in input.

Array, puntatori e stringhe 255



for (i= 0; scanf("%s", word) == 1; ++i) {

ncorpo del ciclo for viene eseguito fintanto che scanf ()

riesce a leggere caratteri dallo standard input e a memorizzarli in word. Si ricordi che scanf () accetta come parametri una stringa di controllo seguita da indirizzi che vengono associati con i formati nella stringa di controllo. Il nome di un array è trattato come un puntatore, quindi non abbiamo bisogno di scrivere &word per indicare l'indirizzo della stringa. Tecnicamente l'utilizzo di &word al posto di word come secondo argomento di scanf ( ) è un errore; tuttavia molti compilatori ignorano l'operatore & utilizzato in questo contesto, e alcuni di essi lo fanno senza fornire alcuna segnalazione. •

if

(i >= N)

error_exit_too_many_words(); if (strlen(word) >= MAXWORD) error_exit_word_too_long(); Se il file contiene troppe parole o ci sono troppi caratteri in una delle parole lette da scanf (),stampiamo un messaggio relativo all'errore riscontrato e usciamo. L'utilizzo di funzioni d'errore in questi controlli contribuisce a rendere più chiaro il codice di mai n (). •

w[i] = calloc(strlen(word) + 1, sizeof(char)); La funzione calloc () si trova nella libreria standard e il suo prototipo in stdlib.h. Una chiamata a funzione come calloc (n, sizeof ( ... ) ) alloca dinamicamente spazio per un array di n elementi, ognuno dei quali richiede sizeof ( ... ) byte di memoria, e restituisce un puntatore allo spazio allocato. Il minimo numero di byte necessari per memorizzare la stringa word, includendo il marcatore di fine stringa \0, è strlen(word)+1 (si noti che il termine +1 è importante: purtroppo viene spesso dimenticato). La funzione calloc ( ) alloca dinamicamente questo spazio e restituisce un puntatore a esso; tale puntatore viene assegnato a w[ i ] . Avremmo potuto invocare malloc () invece di calloc (). Entrambe le seguenti istruzioni avrebbero funzionato correttamente: w[i] = malloc((strlen(word) + 1) * sizeof(char)); w[i] = malloc((strlen(word) + 1));

Le istruzioni sono equivalenti, perché sizeof ( char) ha valore l. Il motivo per cui lo abbiamo evidenziato è quello di ricordare al lettore che nel codice si richiede spazio per memorizzare un array di char. La funzione calloc () inizializza lo spazio allocato a zero, mentre malloc () non esegue nessun tipo di inizializzazione della memoria che alloca. Nel nostro caso dobbiamo sovrascrivere questo spazio e non è quindi richiesta alcuna inizializzazione.

256 Capitolo 6



if (w[i] = NULL) error exit calloc failed(); È una buona-norma di programmazione controllare sempre il valore restituito da calloc ( ) o da malloc ( ) . Se la memoria richiesta non è disponibile viene restituito il valore NULL. In questo caso stampiamo un messaggio d'errore e terminiamo l'esecuzione.



strcpy(w[i], word); Dopo che lo spazio è stato allocato, viene utilizzata la funzione strcpy () per copiare word in memoria a partire dall'indirizzo w[ i]. Alla fine del ciclo for è possibile pensare w come un array di parole.

w

liL~_lil ~ •

sort_words(w, n); /*ordina le parole*/ wrt_words(w, n); /*stampa le parole ordinate*/ la funzione sort_words () viene utilizzata per ordinare in modo lessicografico le parole dell'array, che poi vengono stampate tramite la funzione wrt_words ().

Si consideri ora la funzione sort_words (). Essa utilizza un ordinamento per trasposizione, simile a bubblesort.

Nel file sort.c: #include "sort.h" void sort words(char *w[], int n)

-

{

in t

i, j;

for (i = 0; i < n; ++i) for ( j = i + 1 ; j < n; ++ j)

l* ordina n elementi */

Array, puntatori e stringhe 257

if (strcmp(w[i1, w[j1) > 0)

swap(&w[i1, &w[j1); }

Si osservi che le stringhe puntate da w[ i 1 e w[ j 1 vengono confrontate tramite la funzione st rcmp () e, nel caso non siano in ordine, i due puntatori vengono scambiati utilizzando la funzione swap ( ) . Vengono passati gli indirizzi dei puntatori, in modo che i valori veri e propri dei puntatori nell'ambiente chiamante possano essere modificati dalla funzione chiamata. Le rispettive stringhe in memoria non vengono scambiate: lo scambio avviene solo per i puntatori.

Prima dello scambio

w

Dopo lo scambio w

w[ i]

w[ j]

Nel file swap.c: #include "sort.h" void swap(char **p, char **q) {

258 Capitolo 6

char

*tmp;

tmp = *p; *p = *q; *q = tmp; }

Ognuno degli argomenti &w [ i] e &w [ j ] passati alla funzione swap ( ) è un indirizzo di un puntatore a char o, in maniera equivalente, un puntatore a puntatore a char. Per questo motivo, i parametri formali nella definizione di swap () sono di tipo char **.La funzione swap ( ) è molto simile a quella scritta in precedenza; solo i tipi sono differenti. Si consideri l'istruzione tmp = *p; Essendo p un puntatore a puntatore a char, l'espressione *p che dereferenzia p è di tipo puntatore a char owero char *. Pertanto la variabile tmp è dichiarata di tipo char *

Nel file e"or.c: #include "sort.h" void error exit calloc failed(void) { printf ( "%s" 1 "ERROR: The call to calloc() failed to\n" allocate the requested memory- byel\n"); ex i t ( 1) ; }

void error_exit_too_many_words(void) {

printf("ERROR: At most %d words can be sorted- byel\n" exit(1); }

void error_exit_word_too_long(void) {

printf ( "%s%d%s" 1 "ERROR: A word with more than " 1 MAXWORD 1 "\n" characters was found- byel\n"); exit(1); }

Nel file wrl.c: #include "sort.h" void wrt words(char *w[], int n)

-

{

in t

i• '

1

N);

Array, punta tori e stringhe 259

for (i = 0; i < n; ++i) printf("%s\n", w[i]); }

Naturalmente, volendo utilizzare il programma sorl_words() per un lavoro impegnativo su una grande quantità di dati, sarebbe opportuno utilizzare un algoritmo di ordinamento più efficiente. Sarebbe possibile, per esempio, modificare le funzioni merge (}e merge_sort () per operare su array di puntatori a char anziché su array di int (vedi Esercizio 37). Inoltre, potremmo contare le parole e poi allocare lo spazio per w[ ] dinamicamente, piuttosto che utilizzare un valore N arbitrariamente grande come lunghezza dell'array (vedi Capitolo 11).

6.14

Parametri di main()

In ma in ()è possibile utilizzare due parametri, chiamati convenzionalmente argc e argv, per comunicare con il sistema operativo. Di seguito è riportato un programma che stampa i parametri della propria riga di comando; si tratta di una variante del comando echo di

MS-DOS e UNIX. l* Ristampa i parametri della riga di comando. */

#include int main(int argc, char •argv[]) {

in t

i•t

printf("argc = %d\n", argc); for (i = 0; i < argc; ++i) printf( "argv[%d] %s\n", i, argv[ i]); return 0; }

la variabile argc contiene il numero dei parametri sulla riga di comando. L'array argv è un array di puntatori a char che può essere visto come un array di stringhe, ognuna delle quali è una parola sulla riga di comando. Poiché a rgv [ 0] contiene il nome stesso del comando, argc vale sempre almeno l. Compilando il programma precedente e ponendone il codice eseguibile nel file my_echo, mediante il comando

my_echo a is for apple si ottengono sullo schermo le seguenti righe: argc = 5 argv[0] = my_echo

260 Capitolo 6

argv [ 1 1 = a argv[21 i5 argv[31 for argv [ 41 apple In un sistema MS-DOS, la stringa in argv [ 0] consiste dell'intero nome di percorso del comando, ed è formata da lettere maiuscole. Come mostrato nel Paragrafo 11.12, spesso i nomi dei file vengono passati a mai n () come parametri.

6.15

Array frastagliati

Si desidera ora paragonare gli array bidimensionali di tipo char con gli array monodimensionali di puntatori a char; questi due costrutti presentano sia analogie che differenze. #include in t mai n (voi d) {

char char

a [ 2 ][ 151 = { "abc: " , "a i5 for apple"} ; *p(21 = {"abc: ", "a i5 for apple"};

printf("%c%c%c %5 %5\n", a[0][01, a[0][11, a[0][2], a[0], a[1]); printf("%c%c%c %5 %5\n", p(0][0], p(0][1], p(0][2], p[0], p(1]); return 0; }

Il programma produce il seguente output: abc abc: a is for apple abc abc: a is for apple

Il programma e il suo output mostrano delle analogie nel modo in cui i due costrutti vengono utilizzati. Il programma viene ora considerato più in dettaglio. L'identificatore a è un array bidimensionale; la sua dichiarazione provoca l'allocazione di spazio per 30 char. L'inizializzatore bidimensionale è equivalente a quanto segue: {{'a',

'b',

'c',

':',

'\0'}, {'a',

'

',

'i',

's',

... }}

L'identificatore a è un array i cui elementi sono array di 15 char, quindi a [ 0] e a [ 1 1 sono a loro volta array di 15 c ha r. Poiché gli array di caratteri sono stringhe, a [ 0] e a [ 1 ] sono stringhe. L'array a [ 0] è inizializzato a {'a',

'b',

'c',

':', '\0'}

Array, puntatori e stringhe 261

ed essendo specificati solo cinque elementi, il resto viene inizializzato a zero (carattere nullo). Sebbene in questo programma non vengano utilizzati tutti gli elementi, per esso è stato comunque allocato dello spazio. Il compilatore accede all'elemento a [i 1 [ j 1 utilizzando la mappa di memorizzazione. Ogni accesso richiede una moltiplicazione e un'addizione. L'identificatore p è un array monodimensionale di puntatori a char. La sua dichiarazione fa sì che venga allocato spazio per due puntatori (sulla nostra macchina 4 byte per ogni puntatore). L'elemento p [ 0 l viene inizializzato per puntare ad "ab c: •, stringa che richiede spazio per 5 char. L'elemento p[ 1 1 viene inizializzato per puntare ad •a is ... ",stringa che richiede spazio per 15 char, compreso il carattere nullo '\0' alla fine della stringa; dunque p utilizza meno spazio di a. Inoltre il compilatore non genera codice per la mappa di memorizzazione per accedere a p [ i 1 [ j 1; dunque gli accessi a p sono più veloci rispetto a quelli ad a. Si noti che a [ 0 l [ 141 è un'espressione valida, mentre p [ 0 l [ 14 l non lo è in quanto supera i limiti della stringa puntata da p [ 01. Naturalmente a [ 0 1 [ 14 1 sta al di fuori della stringa a [ 0 1, ma non supera i limiti dell'array a [ 01. Dunque l'espressione a [ 01 [ 141 risulta accettabile. Un'altra differenza è che le stringhe a cui si punta tramite p [ 0 l e p [ 1 1 sono costanti e, di conseguenza, non possono essere modificate. Al contrario, le stringhe a cui si accede con a [ 0 1 e a [ 1 l sono modificabili. Un array di puntatori i cui elementi vengano utilizzati per puntare ad array di varie lunghez7..e è detto array frastagliato. Avendo righe di lunghezza differente, l'array p del programma precedente è un esempio di array frastagliato. Pensando gli elementi p [ i 1 [ j 1 come organizzati in un insieme "rettangolare" in righe e colonne, le differenti lunghezze delle righe danno al "rettangolo" un aspetto frastagliato, da cui il nome "array frastagliato".

Array frastagliato

6.16

Funzioni come parametri

In C, i puntatori a funzioni possono essere passati come parametri, utilizzati in array, restituiti dalle funzioni, e così via. In questo paragrafo viene descritto il funzionamento di questa possibilità.

262 Capitolo 6

Si supponga di dover eseguire un calcolo con una varietà di funzioni. Per esempio, si consideri il calcolo

,. Jr-m

dove dapprima/(k) ... sin(k), e in seguito/è data da/(k) to dalla seguente routine.

=

l/k. Tale compito viene esegui-

Nel file sum_sqr.c: #include "sum_sqr.h" double sum_square(double f(double x), int m, int n) {

in t double

k•,

sum

= 0.0;

for (k m; k y) swap(x, y) order(x, y)

03 (x J y J z)

02 (x J y) ; 02 (x J z) ; 02 ( y ' z )

static yes_no static int

yes_no;

find_pivot(int *left, int *right, int *pivot_ptr); *partition(int *left, int *right, int pivot);

Le macro scritte non sono necessariamente robuste. L'intento è quello di utilizzarle solo in questo file. È stata utilizzata una typedef per rendere il tipo yes_no sinonimo del tipo enum {yes, no}. Poiché le due funzioni f ind_pivot ()e parti tion () hanno classe di memorizzazione static, esse risultano note solo in questo file. void quicksort(int *left, int *right) {

int

*p, pivot;

if (find_pivot(left, right, &pivot) == yes) { p= partition(left, right, pivot); quicksort(left, p- 1); quicksort(p, right); } }

Il preprocessore 347

Di solito quicksort viene implementato ricorsivamente, seguendo l'approccio "divide et impera". Si supponga di dichiarare in ma in () l'array a di lunghezza N. Dopo che l'array è stato riempito, esso può essere ordinato utilizzando la chiamata: quicksort(a, a+ N- 1); Il primo parametro è un puntatore al primo elemento dell'array, il secondo è un puntatore all'ultimo elemento dell'array. Nella definizione di funzione di quicksort () è conveniente pensare che questi puntatori si trovino rispettivamente sul lato sinistro e destro dell'array. La funzione f ind_pivot () sceglie, se possibile, uno degli elementi dell'array come "pivot". la funzione parti tion () viene utilizzata per riorganizzare l'array in modo che la prima parte contenga tutti gli elementi il cui valore è minore del pivot e la parte rimanente tutti gli elementi il cui valore è maggiore o uguale al pivot. Inoltre, part i t io n ( ) restituisce un puntato re a un elemento dell'array. Gli elementi a sinistra di tale puntatore hanno valori minori del pivot, quelli a destra e l'elemento puntato dal puntatore hanno valori maggiori o uguali al pivot. Una volta che l'array è stato riorganizzato rispetto al pivot, quicksort () viene richiamata su ogni sottoarray. static yes_no find_pivot(int *left, int *right 1 int *pivot_ptr) {

int

a, b, c, *p;

a

*left;

l* valore l* valore l* valore l* ordina questi l* il pivot sarà il più alto di

b = *(left + (right- left) 1 2);

c = *right; o3 (a 1 b 1 c);

if

(a < b)

{

*pivot_ptr return yes;

sinistro *l centrale *l destro *l 3 valori *l 2 valori *l

b;

}

if (b < c) { *pivot_ptr = c; return yes; }

for (p= left + 1; p

member_name

Un costrutto equivalente a questo è:

(*pointer_to_structure). member_name Le parentesi sono necessarie. Insieme con ( ) e [ ] , gli operatori "." e -> hanno priorità massima e associatività da sinistra a destra. Dunque il precedente costrutto privato di parentesi risulterebbe equivalente a: * (pointer_to_structure. member_name)

Questo è un errore, in quanto l'operatore ''." può essere utilizzato solo con strutture, non con puntatori a strutture.

Strutture e unioni 367

Viene illustrato ora l'utilizzo di -> scrivendo una funzione che addiziona numeri complessi. Si supponga innanzitutto che in un file d'intestazione sia presente la seguente typedef:

Nel file complex.h: struct complex { double re; double im;

l* parte reale *l l* parte immaginaria *l

}i

typedef

struct complex

complex;

Si scriva poi in un altro file quanto segue.

Nel file 2_add.c: #include Ncomplex.h" voi d add(complex *a, compie x *b, compie x *C)

l* a

=b

+

c *l

{

a -> re a -> i m

b -> re + c -> re; b -> i m + c -> im;

}

Si osservi che a, b e c sono puntatori a strutture. Pensando che essi rappresentino numeri complessi, si addizionano b e c e si pone il risultato in a. Questo è il senso del commento che segue l'intestazione della definizione di funzione. Nella seguente tabella sono illustrati alcuni dei possibili impieghi dei due operatori di accesso ai membri di una struttura. Nella tabella si suppone che il tipo definito dall'utente struct student, presentato in precedenza, sia già stato definito. Dichiarazioni e assegnamenti struct student tmp, *p = &tmp; tmp. gr ade = A tmp.last name = "Casanova•; tmp.student id = 910017; l

l

;

Espressione

Espressione equivalente

Valore concettuale

tmp.grade tmp.last name (*p).student id * p -> last_name + 1 *(p -> last_name + 2)

p -> gr ade p -> last name p -> student id (*(p -> last_name)) (p -> last_name)[2]

A Casanova 910017 + 1

D

s

368 Capitolo 9

9.3

Priorità e associatività degli operatori: una panoramica finale

Nella tabella successiva sono mostrate tutte le regole di priorità e associatività di tutti gli operatori del C. Gli operatori ... " e -> sono stati introdotti in questo capitolo e insieme a ( ) e [ ] hanno priorità massima. Associatività

Operatori ()

[]

->

++ (postjisso)

++ (prefisso) -- (prefisso)

+ (unario) - (unario)

l -

sizeof

& (indirizzo)

(type)

da sinistra a destra da destra a sinistra

* (derejerenziazione)

da sinistra a destra

%

*

-- (postjisso)

da sinistra a destra

+

>

da sinistra a destra

>=

da sinistra a destra

l=

da sinistra a destra

&

da sinistra a destra da sinistra a destra

&&

da sinistra a destra

Il

da sinistra a destra

?:



>>= 1

da destra a sinistra +=

*=

-> ->

next next next

= malloc(sizeof(ELEMENT)); -> ->

d = 'e'; next = NULL;

A questo punto si ha una lista di due elementi.

Lista concatenata di due elementi head~

l

n

l ---t-+ l,..-__-e----rj-Nu_L__,Ll

Viene aggiunto infine l'ultimo elemento:

head head head

-> -> ->

next next next

-> -> ->

next next next

= malloc(sizeof(ELEMENT)); -> d = 'w'; -> next NULL;

=

Si è così ottenuta una lista di tre elementi puntata da head, che termina quando next contiene il valore "sentinella" NULL.

Lista concatenata di tre elementi h e ad

~~e l 3-+1

w

!NULLI

Strutture e trattamento di liste

401

Operazioni sulle liste

10.3

l A' principali operazioni di base sulle liste lineari sono: l. 2. 3. 4. 5. 6.

Creazione di una lista. Conteggio del numero di elementi. Ricerca di un elemento. Concatenazione di due liste. Inserimento di un elemento. Cancellazione di un elemento.

Vengono ora presentate le tecniche per programmare tali operazioni sulle liste mediante l'utilizzo della ricorsione e dell'iterazione. Essendo le liste definite ricorsivamente, l'utilizzo di funzioni ricorsive risulta naturale. Ogni routine richiede le specifiche rwl file list.h. Si osservi che in questi esempi d potrebbe essere ridefinita come una Mtruttura dati arbitrariamente complicata. Come primo esempio viene presentata una funzione che costruisce una lista a partire da una stringa e restituisce un puntatore al primo elemento, o testa, della lista. Il l'Uore della funzione crea un ~emento della lista allocando memoria e assegnando valori ai membri. ' l* Creazione di liste utilizzando la ricorsione. */

#include #include "list.h• LINK string_to_list(char s[]) {

LINK

head;

if (s[0] == '\0') /*caso base*/ return NULL; else { head = malloc(sizeof(ELEMENT)); head ->d= s[0]; head -> next = string_to_list(s + 1); return head; } }

Si noti ancora una volta come la ricorsione abbia un caso base, cioè la creazione della lista vuota, e un caso generale, cioè la creazione del resto della lista. La chiamata ricorsiva del caso generale restituisce come valore un puntatore di tipo LINK alla sottolista rimanente.

402 Capitolo 1o

ANALISI DELLA FUNZIONE string_to_list() •

LINK string_to_list(char s[]) {

LINK head; Passando una stringa come parametro viene creata una lista concatenata dei caratteri di tale stringa. Poiché viene restituito un puntatore alla testa della lista, lo specificatore di tipo nell'intestazione di questa definizione di funzione è LINK.



if (S[0] == \0') l* caso base *l return (NULL); Quando viene raggiunto il marcatore di fine stringa viene restituito NULL e, come mostrato più avanti, la ricorsione ha termine. Il valore NULL viene utilizzato per marcare la fine della lista concatenata.



else { head = malloc(sizeof(ELEMENT)); Se la stringa s [] è diversa dalla stringa vuota, è necessario chiamare malloc () allo scopo di recuperare un numero di byte sufficiente alla memorizzazione di un oggetto di tipo ELEMENT. Poiché malloc () restituisce un puntatore a void, quest'ultimo può essere assegnato alla variabile head, che è un puntatore di tipo differente, senza necessità di un cast. La variabile puntatore he ad contiene ora l'indirizzo del blocco di memoria fornito da malloc ().



head ->d= s[0]; Il primo carattere della stringa s [] viene assegnato al membro d dell'oggetto allocatodi tipo ELEMENT.



head -> next = string_to_list(s + 1); L'espressione puntatore s + 1 punta al resto della stringa. La funzione viene chiamata ricorsivamente con s + 1 come parametro. Al membro puntatore next viene assegnato il valore puntatore restituito da st ring_ to_list ( s + 1). Questa chiamata ricorsi va restituisce come suo valore un LI NK o, in maniera equivalente, un puntatore a ELEMENT che punta alla sottolista rimanente.



return head; All'uscita la funzione restituisce l'indirizzo della testa della lista.

l

Questa funzione può anche essere scritta in maniera iterativa con l'ausilio di un puntato re addizionale di nome t a i l. Per distinguere la versione ricorsi va string_to_list () da quella iterativa, quest'ultima viene chiamata s_to_l().

Nel file iter_list. c: l* Creazione di liste impiegando l'iterazione. *l

#include

Strutture e trattamento di liste 403

#include "list.h" LINK sto l(char s[])

-

{

LINK in t

-

head = NULL, tail; i•l

if (s[0] l= '\0') { /* primo elemento *l head = malloc(sizeof(ELEMENT)); head ->d= s[0]; tail = head; for (i= 1; s[i] l= '\0'; ++i) { /*aggiunge in coda*/ tail -> next = malloc(sizeof(ELEMENT)); tail = tail -> next; tail ->d= s[i]; }

tail -> next = NULL;

/* fine della lista */

}

return head; }

Spesso le funzioni che operano sulle liste richiedono variabili locali di tipo puntato,. come head e tail. Tali puntatori possono essere utilizzati liberamente per rendere il nKiice più semplice. Risulta inoltre importante simulare le routine a mano, ed è utile provare il proprio programma su una lista vuota, su una lista unitaria, cioè una lista di un ••lo elemento, e su una lista di due elementi. Spesso la lista vuota e la lista unitaria rtaultano casi speciali. Il passaggio di una stringa vuota a s _t o_l ( ) crea la lista vuota, in quanto la funzione- restituisce il valore NULL. La creazione del primo elemento viene effettuata dalla prima parte del codice. Nella figura seguente viene mostrata la lista di un elemento ,.,_.ata dalla stringa ·A·; essa rappresenta lo stato della computazione prima che al membro next venga assegnato il valore NULL.

lista di un elemento

headG tailG

~---+: ~~--A----~----7--~

404 Capitolo 1O

Nel caso di due elementi, per esempio • AB", la creazione della lista viene mostrata nelle figure successive. Prima di tutto viene creata la lista unitaria contenente A Quindi viene eseguita l'istruzione f or, con i uguale a l e s [ i] uguale a B Un nuovo elemento viene allocato e attaccato alla lista. l

l

l •

l •

Viene attaccato un secondo elemento

headG tailG L'istruzione tail = tail -> next; sposta tail sul nuovo elemento, al cui membro d viene poi assegnato B l

l •

Aggiornamento di tail

he

ad Gf------+~ IL---A-----'---•_____,

B

?

t ai l

Ora s [ 2] vale \0 e all'uscita dall'istruzione f or si ha una lista di due elementi. La fine della lista viene quindi marcata con un NULL. Poiché malloc () non deve inizializzare la memoria a zero, durante alcuni passi della computazione si hanno dei membri con valori indefiniti.

Strutture e trattamento di liste 405

Dopo l'assegnamento di NULL

B

10.4

NULL

Alcune funzioni per l'elaborazione di liste

Vmgono ora presentate altre due funzioni ricorsive: la prima conta gli elementi di una

hla, mentre la seconda li stampa. Entrambe attraversano la lista in maniera ricorsiva IAnu a trovare il puntatore NULL. Tutte e due le funzioni utilizzano il file d'intestazione IMI.le. La funzione count () restituisce O se la lista è vuota, altrimenti restituisce il numeru di t>lementi della lista. l* Conta ricorsivamente gli elementi di una lista *l

int count(LINK head) {

if (head == NULL) return 0; else return (1 + count(head

->

next));

Una versione iterativa di questa funzione sostituisce la ricorsione con un ciclo for. l* Conta iterativamente gli elementi di una lista */

int count it(LINK head) { int cnt = 0; for

; head l= NULL; head ++cnt; return cnt;

}

head

->

next)

406 Capitolo 1o

Si ricordi che he ad viene passato "per valore"; pertanto la chiamata count_i t () non modifica il puntatore di accesso alla lista nell'ambiente chiamante. La routine print_list () attraversa una lista ricorsivamente stampando il valore della variabile membro d. /* Stampa una lista ricorsivamente */

void print_list(LINK head) {

if (head == NULL) printf ( "NULL"); else { printf("%c --> ", head ->d); print_list(head -> next); } }

Per presentare l'utilizzo di queste funzioni, viene scritto un programma che converte la stringa "ABC • in una lista e la stampa.

Nel file prn_list.c: #include #include #include "list.h" LINK void int

string_to_list(char [)); print_list(LINK); count(LINK);

int main(void) {

LINK



'

h= string_to_list("ABC"); printf("The resulting list is\n"); print_list(h); printf("\nThis list has %d elements.\n", count(h)); return 0; }

n programma produce l'output seguente: The resulting list is A --> B --> C --> NULL This list has 3 elements. Spesso si desidera ottenere un'unica lista a partire da due liste distinte. La concatenazione delle liste a e b, nell'ipotesi che a non sia vuota, è la lista ottenuta attaccando la

Strutture e trattamento di liste 407

lh,ta balla fine della lista a. Una funzione per la concatenazione può attraversare la lista rt'rcandone la fine, marcata dal puntatore nullo, e, tenendo traccia dell'ultimo puntato"' non nullo, attaccare la lista b allink next dell'ultimo elemento di a. 1

l* Concatena le liste a e b con a in testa. */

void concatenate(LINK a, LINK b) {

assert(a l= NULL); if (a -> next == NULL) a -> next = b; else concatenate(a -> next, b); }

Utilizzando la ricorsione è possibile evitare l'utilizzo di puntatori ausiliari per muovt•rsi nella lista a. In generale, rimpiego della ricorsione risulta del tutto naturale grazie al carattere autoreferenziante dell'elaborazione di liste. La forma generale di queste funzioni ricoraivt• è la seguente: void generic_recursion(LINK head) {

if (head == NULL)

esegui il caso base else

esegui il caso generale e opera una ricorsione con generic_resursion(head -> next)

Inserimento Una delle proprietà più utili delle liste è rappresentata dal fatto che l'inserimento di un t•lemento richiede una quantità di tempo costante, una volta che sia stata determinata la JX>sizione nella lista. Al contrario, nel caso si volesse porre un valore in un array con molti elementi, mantenendo tutti gli altri valori nel medesimo ordine sequenziale, l'inserimento richiede in media un tempo proporzionale alla lunghezza dell'array: infatti i valori di tutti gli t•lementi dell'array che seguono l'elemento appena inserito devono essere spostati di una posizione. Di seguito è presentato l'inserimento di un nuovo elemento puntato da q tra due elementi adiacenti di una lista puntati da p1 e p2.

408 Capitolo 1O

Prima dell'inserimento

Dopo l'inserimento o

o

c

o---+

••-+--+111

o

o

o

p1d) La seguente funzione insert () pone l'elemento puntato da q tra gli elementi puntati da p1 e da p2: l* Inserimento di un elemento in una lista concatenata. */

void insert(LINK p1, LINK p2, LINK q) {

assert(p1 -> next p1 -> next = q; q -> next = p2;

p2); /* inserimento */

}

Cancellazione La cancellazione di un elemento da una lista lineare concatenata risulta estremamente semplice. Al membro link del predecessore dell'elemento da cancellare viene assegnato l'indirizzo del successore dell'elemento cancellato. Ricorrendo di nuovo alla rappresentazione grafica, lo schema iniziale è il seguente.

Strutture e trattamento di liste 409

Prima deHa cancellazione

.. ·-+l p



A

cf)

l

~l

B



l

~l

c



c



~

...

Viene ora eseguito il seguente codice: p -> next

=p

->

next

->

next;

Dopo la canceHazione

.. ·-+l p

cf)

A

f

B



1~1

~

...

Come mostrato nel diagranuna, l'elemento contenente B non risulta più accessibile t•d è quindi inutile; tale tipo di elemento è detto garbage (sporcizia). Poiché la memoria ,. . spesso una risorsa critica, è opportuno che quest'area venga restituita al sistema per t•ssere riutilizzata, e a tale scopo è possibile utilizzare la funzione free ( ) in stdlib.h. Mediante la chiamata l

free (p)

l

;

lo spazio di memoria precedentemente allocato per l'oggetto puntato da p viene reso disponibile al sistema. Il parametro formale di free () è un puntatore a void. Utilizzando free ( ) è possibile scrivere una routine di cancellazione che restituisca al sistema la memoria allocata per la lista. l* Cancellazione ricorsiva di una lista. *l

void delete list(LINK head) { if (head != NULL) { delete list(head -> next); free(head); } }

l* rilascia la memoria *l

41 O Capitolo 1O

Poiché free () riceve un unico parametro di tipo void *, a tale funzione può essere passato un puntatore di qualunque tipo. Non è necessario utilizzare un cast in quanto void * è il tipo puntatore generico.

10.5

Stack

Nel Paragrafo 9.10 viene discusso il tipo stack come array. In questo paragrafo viene presa in considerazione un'implementazione dello stesso ADT mediante liste lineari concatenate. Nel caso di uno stack, l'accesso è ristretto alla testa della lista, chiamata in questo caso top (cima). Inoltre, gli inserimenti e le cancellazioni awengono solo al top, mediante le operazioni dette rispettivamente push e pop. Uno stack può essere visualizzato come una pila di vassoi; un vassoio viene sempre posto in cima alla pila o prelevato dalla cima. Graficamente, gli stack vengono raJF presentati verticalmente.

Implementazione di uno stack

Viene ora presentato un programma che implementa uno stack e che consiste in un file .h e due file .c. Di seguito è riportato il file d'intestazione.

Strutture e trattamento di liste

411

Nel file stack.h:

l* Implementazione di uno stack mediante una lista concatenata. *l #include #include #define #define

EMPTY FULL

typedef typedef

char enum {false, true}

struct elem { data struct elem

0 10000

data; boolean;

l* un elemento dello stack *l

d·, •next;

} j

typedef

struct elem

elem;

struct stack { int cnt; elem *top;

l* contatore degli elementi */ l* puntatore al top */

};

typedef

struct stack

voi d voi d data data boolean boolean

initialize(stack •stk); push(data d, stack *stk); pop(stack •stk); top(stack •stk); empty(const stack •stk); full(const stack *stk);

stack;

l prototipi di funzione delle sei operazioni standard del tipo stack sono elencati in fondo

al file d'intestazione. Concettualmente queste funzioni si comportano come quelle pre~ntate nel Paragrafo 9.10; l'impiego del tipo data rende il codice riutilizzabile (esso vit•ne utilizzato anche nel Paragrafo 10.6).

Nel file stack.c:

l* Le routine base per uno stack. */ #include "stack.h" void initialize(stack *stk) {

stk -> cnt stk -> top

=

0; NULL;

}

void push(data d, stack •stk)

412 Capitolo 1O

{

elem

*p;

p= malloc(sizeof(elem)); p -> d = d; p -> next = stk -> top; stk -> top = p; stk -> cnt++; }

data pop(stack •stk) {

data elem

d; *p;

d = stk -> top -> d; p = stk -> top; stk -> top = stk -> top -> next; stk -> cnt--; free(p); return d; }

data top(stack •stk) {

return (stk ->top-> d); }

boolean empty(const stack •stk) {

return ( ( boolean) ( stk -> cnt

EMPTY));

}

boolean full(const stack •stk) {

return ( ( boolean) ( stk -> cnt == FULL)) ; }

La routine pus h () richiama malloc () per creare un nuovo elemento dello stack; pop () restituisce al sistema lo spazio di memoria liberato. Uno stack è una struttura dati LIFO (last-in-first-out, l'ultimo a entrare è il primo a uscire). Pertanto, ponendo sullo stack prima una ' a' e poi una 'b', pop ( ) preleverà prima ' b ' . Questa proprietà viene utilizzata in mai n ( ) per stampare una stringa in ordine inverso; ciò è utile come prova per l'implementazione dell'ADT stack.

Nel file mai n. c: l* Prova l'implementazione di uno stack rovesciando una stringa. */ #include "stack.h" int main(void)

Strutture e trattamento di liste 413

char in t stack

str[ 1 i;

"My name is Joanna Kelleyl";

s;

initialize(&s); /* inizializza lo stack */ printf(" In the string: %s\n", str); f or ( i = 0 ; s t r [ i 1 l = 0 ++i) if ( lfull(&s)) push(str[i1, &s); /*pone un char sullo stack *l pr intf ( "From the stack: ") ; while (lempty(&s)) putchar(pop(&s)); /*preleva un char dallo stack */ putchar( \n •); return 0; l

\

l

;

l

Si osservi che la funzione main () è del tutto simile a quella scritta nel Paragrafo 9.10. S.•bbene qui lo stack sia implementato come una lista concatenata e nel Paragrafo 9.10 rome una stringa, l'utilizzo delle operazioni è simile. Di seguito è riportato l'output del proarramma: In the string: My name is Joanna Kelleyl From the stack: lyelleK annaoJ si eman yM

Un esempio: notazione polacca e valutazione di uno stack

10.6

l.a notazione ordinaria utilizzata per scrivere le espressioni, nella quale gli operatori M'parano gli argomenti, è chiamata infissa. Un'altra notazione per le espressioni, utile JM'r la valutazione basata su stack, è detta notazione polacca o priva di parentesi. Nella 11otazione polacca gli operatori si trovano dopo gli argomenti. Per esempio: 3, 7, +

è• equivalente a: 3

+

7

Nella notazione polacca, spostandosi da sinistra a destra, gli operatori possono essere applicati appena vengono incontrati, dunque 17, 5, 2, *, +

è• equivalente a: 17 + (5 * 2)

414 Capitolo 1O

Un'espressione polacca può essere valutata con un algoritmo che utilizza due stack: lo stack polacco, che contiene l'espressione polacca, e lo stack di valutazione, che memorizza i valori intermedi durante l'esecuzione. Nella tabella seguente viene presentato un algoritmo a due stack che valuta le espressioni polacche con operatori tutti binari.

Algoritmo a due stack per la valutazione di espreulonl polacche Se lo stack polacco è vuoto, terminare dando come risultato il top dello stack di valutazione. Se lo stack polacco non è vuoto, eliminare il valore al top dello stack polacco e porlo in d. (Vengono utilizzati d, d1 e d2 per memorizzare dati.) Se d è un valore, va posto nello stack di valutazione. Se d è un operatore, è necessario prelevare i due elementi in cima allo stack di valutazione ponendo il primo in d2 e il secondo in d1. L'operazione in d va applicata a d1 e a d2; il risultato va posto sullo stack di valutazione. Ritornare al passo l.

l. 2. 3. 4.

L'algoritmo viene illustrato nel seguente diagramma, nel caso della valutazione dell'espressione: 13, 4, -, 2, 3, *, +

Algoritmo a due stack per la valutazione di espressioni polacche

s a c k

s a c k p o l a c c o

d

v

a l u t a z i o n e

13 4

4

2 3 *

2 3 *

+

+

2 3 * 13

+

4 13

2 3 * +

3 * 9

+

2 9

* +

3 2 9

+

6 9

15

Strutture e trattamento di liste

415

Viene ora presentato un programma che implementa questo algoritmo a due stack. Un'idea chiave è quella di ridefinire data in modo che possa memorizzare un valore in fom1a di intero o un operatore in forma di carattere. Il programma è formato da un file .h r da cinque file .c. Di seguito è riportato il file d'intestazione:

Nel file po/ish.h: l* Implementazione di uno stack polacco mediante una lista concatenata. *l

#include #include #include #include #define #define



EMPTY

0 10000

FULL

struct data { enum {operator, value} union { char op; int val; }

kind;

u;

};

typedef typedef

struct data enum {false, true}

struct elem { data struct elem

data; boolean;

l* un elemento dello stack *l d;

*next;

};

typedef

struct elem

st ruct stack { int cnt; elem •top;

elem;

l* contatore degli elementi *l l* puntatore al top *l

};

typedef boolean in t voi d boolean voi d data voi d voi d voi d data

struct stack stack; empty(const stack •stk); evaluate(stack *polish); fill(stack •stk, const char *str); full(const stack •stk); initialize(stack •stk); pop(stack •stk); prn_data(data *dp); prn_stack(stack •stk); push(data d, stack •stk); top(stack *stk);

416

Capitolo 1O

Si noti che questo file d'intestazione è simile a quello utilizzato nel programma dello stack del Paragrafo 10.5. La differenza fondamentale è che data è definito come tipo struttura. Tale struttura contiene una union in grado di memorizzare un valore int o un operatore in forma di char e un "flag" in forma di tipo enumerativo. Il flag indica quale tipo di dato sia memorizzato.

Nel file main. c:

/* Verifica l'algoritmo a due stack per la valutazione di espressioni polacche. */ #include "polish.h" int main(void) {

char stack

str[] = "13, 4,polish;

2, 3, •, +";

printf("\n%s%s\n\n", "Polish expression: ", str); fill(&polish, str); /* riempie lo stack da una stringa */ prn_stack(&polish); /*stampa lo stack */ printf("\n%s%d\n\n", "Polish evaluation: " evaluate(&polish)); return 0; }

In mai n (), lo stack polacco viene riempito in base a una stringa e stampato al fine di verificare che le operazioni siano corrette. La funzione più interessante è quella che valuta lo stack polacco, presentata qui di seguito.

Nel file eva l. c: l* Valutazione di uno stack polacco */

#include "polish.h" int evaluate(stack •polish) {

data stack

d, d1' d2; eva!;

initialize(&eval); while (lempty(polish)) { d= pop(polish); switch (d.kind) { case value: push(d, &eva!); break;

Strutture e trattamento di liste 417

case operator: d2 = pop(&eval); d1 = pop(&eval); d.kind = value;

l* 1n1z1o della riscrittura di d *l

switch (d.u.op) { case l+ l: d.u.val d1.u.val + d2.u.val; break; case l- l: d.u.val = d1.u.val d2.u.val; break; case * d.u.val = d1.u.val * d2.u.val; 1

1

:

}

push(d, &eva!); } }

d= pop(&eval); return d.u.val; }

l.a funzione evaluate() incorpora l'algoritmo a due stack presentato sopra. Prima di lutto d viene prelevata dallo stack polish. Se essa è un valore viene posta nello stack eva!, se è un operatore è necessario prelevare d2 e d1 dallo stack eva! ed eseguire l'operazione indicata, ponendone il risultato sullo stack eva!. Quando lo stack polish rhmlta vuoto, d viene prelevato dallo stack eva! e viene restituito come risultato il valon· int d. u. val. Le operazioni sullo stack vengono scritte nel file stack.c. A parte l'inc:lusione di un file d'intestazione diverso, non vi sono differenze rispetto al file stack.c discusso nel Paragrafo 10.5. Nel file stack.c: l* Le routine base per uno stack. *l

#include "polish.h" void initialize(stack *stk) {

stk -> cnt 0; stk -> top = NULL; }

void push(data d, stack *stk) {

elem

*p;

p= malloc(sizeof(elem)); p -> d

= d;

p -> next = stk stk -> top = p;

->

top;

418 Capitolo 1O

stk -> cnt++; }

Nell'implementazione dello stack presentata nel Paragrafo 10.5, il tipo data risulta equivalente a char; in questo caso data è un tipo struttura. Utilizzando una typedef per incorporare l'idea di "dato", si è ottenuta un'implementazione riutilizzabile dell'ADT stack. Questo punto è fondamentale. Utilizzando codice già scritto e provato si realizza un rispannio di lavoro sul progetto corrente. E necessario poter riempire uno stack da una stringa contenente un'espressione polacca. Di seguito è riportata la funzione che compie questa operazione.

Nel file fili. c: #include "polish.h" void fill(stack •stk, const char •str) {

const char char boolean data stack

•p = str; c1, c2; b1, b2; d·, tmp;

in i tialize ( stk); initialize(&tmp); l* Il Elabora prima di tutto la stringa e pone i dati su tmp. *l while (*p l= '\0 while (isspace( *p) Il •p == l, l) ++p; b1 = (boolean) ((c1 =*p)== + Il c1 == '-' Il c1 == b2 = (boolean) ((c2 =*(p+ 1)) ==·,~Il c2 == \0 i f ( b 1 && b2) { d.kind operator; d.u.op c1; 1

{

)

1

1

l •');

1

1

);

}

else { d.kind = value; assert(sscanf(p, "%d", &d.u.val) == 1); }

if ( lfull(&tmp)) push(d, &tmp); while (*p l= && •p l= '\0 ++p; l,

l

l* pone i dati su tmp *l 1 )

}

l* Il Ora preleva dati da tmp e li pone su stk. *l

Strutture e trattamento di liste 419

while (lempty(&tmp)) { d= pop(&tmp); if ( lfull(stk)) push(d, stk);

l* preleva dati da tmp *l l* pone i dati su stk *l

}

In primo luogo viene elaborata una stringa per estrarne i dati, che, una volta trovati, vc·ngono posti sullo stack tmp. Terminata l'elaborazione della stringa, i dati vengono J)ft•levati da tmp e posti sullo stack puntato da stk. In questo modo gli elementi nello alnck puntato da stk sono in ordine corretto. Vengono ora presentate due funzioni di stampa, utili per controllare che il codice funzioni correttamente. Nel file print.c: #include "polish.h" void prn data(data *dp)

-

{

switch (dp -> kind) { case operator: printf("%s%3c\n", "kind: operator break; case value: printf(•%s%3d\n", "kind: value

op:", dp

->

u.op);

val:", dp

->

u.val);

} }

void prn_stack(stack •stk) {

data

d;

printf(ustack count:%3d%su, stk -> cnt, (stk -> cnt == 0) ? "\n" : • ") ; if ( lempty(stk)) { d= pop(stk); l* preleva il dato *l l* stampa il dato *l prn_data (&d); prn stack(stk); l* chiamata ricorsiva *l push(d, stk); l* inserisce il dato *l }

L'algoritmo per stampare lo stack è molto semplice; prima preleva d dallo stack e successivamente lo stampa, quindi chiama ricorsivamente prn_stack () e, infine, pone nuovamente d sullo stack. L'effetto è quello di stampare tutti i dati nello stack, }asciandolo nello stato originale.

420 Capitolo 1O

Di seguito è riportato l'output del programma: Polish expression: 13, 4, - J 2, 3, * J + stack stack stack stack stack stack stack stack

count: count: count: count: count: count: count: count:

7 6 5 4 3 2

1 0

kind: kind: kind: kind: kind: kind: kind:

value value operator value value operator operator

val: 13 val: 4 op: val: 2 val: 3 op: * op: +

Polish evaluation: 15

10.7

Code

Un a coda è un altro esempio di tipo di dato astratto (AD1) implementabile come lista lineare concatenata. Una coda ha due estremità: una testa e una coda. Gli elementi vengono inseriti in coda e prelevati in testa.

Implementazione di una coda

queue cnt front rear -

~

elem

el em

-+

elem

~oo--ro~~-·--~1 ~~~_oo__~--~-·--~1 ~~~_oo__ro~~N_u_LL~ L'implementazione dell'ADT qui presentata contiene alcune delle funzioni standard sulle code. Di seguito è presentato il file d'intestazione.

Strutture e trattamento di liste

421

Nel file queue.h: l* Implementazione di una coda mediante lista concatenata. */

#include #include #include #define #define

EMPTY FULL

typedef typedef

unsigned int enum {false, true}

struct elem { data struct elem

0 10000

data; boolean;

l* un elemento della coda */ d;

*next;

};

typedef struct elem struct queue { in t cnt; elem *front; elem *rear;

elem;

l* contatore degli elementi */ l* puntatore alla testa */ l* puntatore alla coda *l

};

typedef voi d voi d data data boolean boolean

struct queue queue; initialize(queue *q); enqueue(data d, queue *q); dequeue(queue *q); front(const queue *q); empty(const queue *q); full(const queue *q);

Alla fine del file d'intestazione viene posto l'elenco dei prototipi di funzione. Le definizioni di funzione vengono scritte nel file queue.c. Queste funzioni, insieme a questo file d'intestazione, implementano l'ADT coda.

Nel file queue.c:

l* Le routine di base relative alle code. */ #include "queue.h" void initialize(queue *q) {

q -> cnt = 0; q -> front = NULL; q -> rear = NULL; }

data dequeue(queue *q)

422 Capitolo 1O

{

data elem

d; *p;

d = q -> front -> d; p = q -> front; q -> front = q -> front -> next; q -> cnt--; free(p); return d; }

void enqueue(data d, queue *q) {

elem

*p;

p= malloc(sizeof(elem)); p -> d = d;

p -> next = NULL; i f ( l empt y (q ) ) { q -> rear -> next q -> rear = p;

p;

}

else q -> front q -> cnt++;

q -> rear

p;

}

data front(const queue *q) {

return (q-> front-> d); }

boolean empty(const queue *q) {

return ((boolean) (q-> cnt -- EMPTY)); }

boolean full(const queue *q) {

return ( (boolean) (q -> cnt == FULL)); }

La routine enqueue ( ) utilizza l'allocatore di memoria malloc () per creare un nuovo elemento della coda; la routine dequeue ( ) restituisce al sistema lo spazio di memoria liberato. Una coda è una struttura dati FIFO (jirst-in-jirst~ut, il primo a entrare è il primo a uscire). Il primo elemento posto nella coda sarà il primo a essere rimosso. Le code risultano estremamente utili in svariate applicazioni relative alla programmazione di sistema. Per esempio, esse vengono spesso utilizzate per la schedulazione di risorse nei sistemi operativi e per la scrittura di simulatori di eventi.

Strutture e trattamento di liste 423

Come esempio viene presentato uno schedulatore elementare per un sistema a

dut• processori. Si suppone che un processo possa richiedere un servizio dal processore Ao dal processore B. Un processo viene individuato da un proprio numero identificaton- (PIO). Dopo aver letto tutte le richieste, lo schedulatore stampa l'ordine con cui i pnx.·essi vengono serviti da ogni processore.

Nel file main.c:

l* Utilizzo delle code per schedulare due risorse. *l #include "queue.h• int main(void) {

in t in t in t data queue

c; cnt a cnt-b pidT a, b;

0; 0; l* numero PIO *l

initialize(&a); initialize(&b); l* Accoda le richieste. *l while ( (c = getchar ()) l= EOF) { switch (c) { case 'A': assert(scanf("%u", &pid) if (lfull(&a)) enqueue(pid, &a); break; case 'B': assert(scanf("%u", &pid) i f ( l fu 11 ( &b ) ) enqueue(pid, &b);

1);

1);

} }

l* Preleva le richieste dalla coda e le stampa. *l printf("\nA's schedule:\n"); while (lempty(&a)) { pid = dequeue(&a); printf(" JOB %u is %d\n", ++cnt_a, pid); }

printf("\nB's schedule:\n"); while (lempty(&b)) { pid = dequeue(&b); printf(• JOB %u is %d\n", ++cnt_b, pid); }

return 0; }

424 Capitolo 1O

Per provare il programma viene creato il seguente file di input:

Nel file input: B A B A A

7702 1023 3373 5757 1007

Impartendo il comando

scheduler


d); preorder(root -> left); preorder(root -> right); } }

void postorder(BTREE root) {

if (root l= NULL) { postorder(root -> left);

Strutture e trattamento di liste 427

postorder(root -> right); printf("%c ", root ->d); } }

l• visita in ordine anticipato dell'albero binario mostrato in precedenza produce come uutput G D B A C F E I

H J

ntc·ntre quella in ordine posticipato produce:

A C B E F D H J

I

G

Si suggerisce al lettore che non abbia familiarità con questi metodi di verificare con n.ara questi risultati con simulazioni manuali. La visita è il centro di quasi tutti gli algoritmi che manipolano gli alberi.

Costruzione di alberi \'Nane presentata ora la costruzione di un albero binario a partire dai valori memorizzati ln un array. Come nel caso delle liste, viene utilizzata la funzione per l'allocazione dinamica di memoria malloc (). /* Costruzione di un albero binario. */

BTREE new node(void)

-

{

return (malloc(sizeof(NODE))); }

BTREE init_node(DATA d1, BTREE p1, BTREE p2) {

BTREE

t;

t= new node(); t->d;-d1; t-> left = p1; t -> right = p2; return t; }

Queste routine vengono utilizzate come primitive per la costruzione di un albero binario a partire dai valori memorizzati in un array. Gli indici di un array lineare possono t·ssere elegantemente fatti conispondere a nodi di un albero binario. Ciò viene effettualo prendendo il valore a [i] e considerando come suoi figli i valori a [ 2* i +1 1 e a [ 2* i +2 1. Nt•ll'unico nodo radice dell'albero binario risultante viene mappato a [ 01. ll suo figlio sinistro è a [ 1] e il suo figlio destro a [ 2]. La funzione create_ t ree () incorpora questa mappa. Il parametro formale size è il numero di nodi dell'albero binario.

428

Capitolo 1O

l* Costruzione di un albero binario da un array. */

BTREE create_tree(DATA a[], int i, int size) {

if (i >= size) return NULL; else return (init_node(a[i], create_tree(a, 2 *i+ 1, size), create_tree(a, 2 * i+ 2, size))); }

10.9

Liste concatenate generali

Per alcune strutture dati, l'utilizzo di array viene combinato con quello di liste; gli array permettono un accesso diretto, mentre le liste permettono un accesso sequenziale. Come esempio, in questo paragrafo viene mostrata l'implementazione di un albero generale, cioè di un albero in cui un nodo può avere un numero di figli arbitrario. L'impiego di una struttura che utilizzi il massimo numero di link per ogni nodo risulterebbe estremamente dispendioso. Un albero generale viene rappresentato mediante alcune liste lineari concatenate, una per ogni nodo. Ogni lista è il figlio di un unico nodo. Alle liste viene associato un array i cui elementi puntano al primo figlio del nodo corrispondente, mentre l'elemento base dell'array punta al nodo radice. Nel diagramma seguente viene mostrata la rappresentazione del caso dell'albero generale discusso all'inizio del Paragrafo 10.8.

Albero generale e struttura a lista associata

t[0]G

lilla

b/

/ / /77e/g NULL NULL NULL NULL

f-------.

/ NULL

h

Strutture e trattamento di liste 429

Gli alberi generali possono essere rappresentati per mezzo del seguente file d'inlrltazione:

Nel file gtree.h: #include #include #include typedef

char

struct node { in t DATA struct node }; typedef tupedef

DATA; child no;

d;

-

*sib;

struct node NOCE

NOCE; *GTREE;

#include "fct_proto.h"

/* prototipi di funzione */

Viene utilizzato un array t di tipo GTREE, in cui t [ 0] punta all'elemento radice rappresentato come tipo NOCE. I fratelli sono raggiungibili seguendo il cammino sulla URta lineare, e i figli indicizzando l'array. Viene ora esaminata la creazione di tale albero particolare, cominciando dalle routine per la creazione di un singolo nodo. l* Creazione di un nuovo nodo. */

GTREE new_gnode(void) {

return (malloc(sizeof(NODE))); }

GTREE init_gnode(DATA d1, int num, GTREE sibs) {

GTREE

tmp;

tmp new_gnode(); tmp ->d= d1; tmp -> child_no = num; tmp -> sib = sibs; return tmp; }

Queste routine possono essere utilizzate per creare l'albero del precedente diarramma. Poiché esso contiene otto nodi, è necessario un array t [ ] di dimensione 9 e di lApo NOCE, in cui t [ 0] è il puntatore alla radice.

430 Capitolo 1O

t[0] = init_gnode('a', 1, NULL); t [ 1 ] = init_gnode('b', 2, NULL); t [ 1 ] -> sib = init_gnode('f', 6, NULL); t [ 1] -> sib -> sib = init_gnode( 'h', 8, NULL); t[2] = init_gnode('c' 1 3 1 NULL); t[2] -> sib = init_gnode( 'd', 4, NULL); t[2] -> sib -> sib = init_gnode('e' 1 5 1 NULL); NULL; t[3] NULL; t[4] NULL; t[5] init_gnode('g', 7, NULL); t[6] NULL; t[7] NULL; t[8] Utilizzando questa rappresentazione, è facile contare il numero di figli di un nodo, o verificare se un nodo sia una foglia. Infatti, se t [n] è NULL allora il nodo n è una foglia.

Visita La visita agli alberi generali è una combinazione di un movimento lungo le liste e di un accesso agli elementi dell'array che puntano alle liste. È abbastanza semplice generalizzare le visite a queste strutture in ordine anticipato, posticipato e simmetrico. Ancora una volta, questi algoritmi sono prototipi per funzioni più complicate applicabili a un albero, in quanto garantiscono che ogni elemento sia raggiunto in tempo lineare.

l* Visita in ordine anticipato di alberi generali. *l void preorder_g(GTREE t, int ind) {

GTREE

tmp;

l* tmp attraversa la lista dei fratelli *l

tmp = t[ind]; l* t[ind] è il nodo radice *l while (tmp 1= NULL) { printf("%c %d\n" 1 tmp ->d, tmp -> child_no); preorder_g(t, tmp -> child_no); tmp = tmp -> sib; }

La funzione preorder _g () differisce dalla funzione corrispondente per il caso di alberi binari poiché, per muoversi lungo la lista lineare dei fratelli, è necessario un ciclo while. Si noti che la ricorsione permette di gestire in maniera pulita ogni sottoalbero.

Utilizzo di calloc() e costruzione di alberi La funzione di libreria calloc () permette l'allocazione di memoria contigua utilizzabile per gli array. Il suo prototipo di funzione si trova in stdlib.h come: void •calloc(size_t n, size_t size);

Strutture e trattamento di liste 431

lamrametri di calloc () vengono dunque convertiti al tipo size_ t, e il valore restituito f di tipo puntatore a void. Normalmente size_t è equivalente a unsigned. Una chia-

mata di funzione della forma calloc (n, size) rrstituisce un puntatore a uno spazio di memoria contiguo sufficiente per n oggetti, clllfluno di size byte. Tale spazio viene inizializzato a zero dal sistema. Dunque calloc () può essere utilizzata per allocare dinamicamente spazio per array definiti durante l'ese&.-uzione. Ciò è utile per allocare solamente lo spazio necessario senza dover specificare durante la compilazione la lunghezza dell'array, che potrebbe essere notevole, per polrr gestire tutti i casi possibili. Come esempio viene considerata una routine per la costruzione di un albero generalt• da una lista di archi e da un array di tipo DATA. Volendo un array di lunghezza 10 per nwmorizzare i puntatori ai sottoalberi, è possibile scrivere: t= calloc(10, sizeof(GTAEE)); l.'array t di tipo puntatore a GTAEE, allocato dinamicamente, può essere passato a una funzione bui ldt ree ( ) per la costruzione di un albero generale. Questa funzione riceve una rappresentazione di un albero come lista di archi e ricava la sua rappresentazione l·ome struttura a lista generale.

l* Function buildtree: crea un albero da un array di archi. */ typedef struct { int out; in t in; } PAIA;

l* PAIA rappresenta un arco nell'albero *l

void buildtree(PAIA edges[], DATA d[], int n, GTAEE t[]) {

in t in t

i•J

x, y;

l* estremi di un arco *l

t[0] = init_gnode(d[1], 1, NULL); /* t[0] considera il nodo 1 come radice */ far (i= 1; i 0) { for (p = mybuf; p - mybuf < n; ++p) if (islower(*p)) *p= toupper(*p); else if (isupper(*p)) *p= tolower(*p); write(out_fd, mybuf, n); }

close(in_fd); close(out fd); return 0;}

ANALISI DEL PROGRAMMA change_case •

#include #include #include /* in MS-DOS si utilizzi io.h */ Il file d'intestazione {enti. h contiene costanti simboliche che vengono utilizzate nel programma, il file d'intestazione unistd.h contiene i prototipi di funzione per open ( ) e re ad (). In MS-DOS è invece necessario includere io. h.



in_fd = open(argv[1], O_RDONLY); Il primo parametro di open () è un nome di file; il secondo parametro specifica come il file debba essere aperto. Se non ci sono errori la funzione restituisce un descrittore di file, altrimenti viene restituito -l. L'identificatore in_fd è mnemonico e sta per "in file descriptor". Sia in MS-DOS che in UN IX, la costante simbolica O_RDONL Y è fornita infcntl.h, e il suo nome sta per "open for reading only", apri in sola lettura.



out_fd = open(argv[2], O_WRONLY l O_EXCL l O_CREAT, 0600); Le costanti simboliche in {enti. h utilizzate per aprire un file possono essere combinate con un operatore di OR bit a bit. In questo caso viene specificato che il file viene aperto solo per la scrittura, che il file deve essere aperto in esclusiva (cioè che si ha un errore se il file è già stato aperto) e che il file deve essere creato se non esiste. O_EXCL viene usata solo con O_CREAT. Se il file viene creato, il terzo parametro definisce i permessi relativi al file; altrimenti esso non ha alcun effetto. I vari tipi di permessi vengono presentati nel seguito.

462 Capitolo 11



while ((n= read(in_fd, mybuf,

BUFSIZE))

>

0) {

Dal file associato a in_fd vengono letti e posti in mybuf un massimo di BUFSIZE caratteri; viene restituito il numero di caratteri letti. Il corpo del ciclo while viene eseguito fino a quando rea d ( ) è in grado di ricevere caratteri dal file. Nel corpo del ciclo le lettere contenute in mybuf vengono convertite da maiuscole in minuscole e viceversa. •

write(out_fd, mybuf, n); Nel file associato a out_ fd vengono scritti n caratteri di mybuf.



close(in_fd); close(out_fd);

Queste istruzioni chiudono i due file. Senza un'esplicita chiusura da parte del programmatore, i file vengono comunque chiusi all'uscita dal programma.

Attenzione: questo programma non è user-friendly ("amichevole con l'utente''). Fornendo il comando change_case

file1

file2

se file2 esiste, esso non viene riscritto come ci si aspetterebbe: un programma progettato dovrebbe informare l'utente di ciò.

11.9

Permessi di accesso ai file

In UNIX, un file viene creato con associati alcuni permessi, che ne determinano le possibilità di accesso da parte del proprietario, del gruppo e degli altri. L'accesso può re in lettura, scrittura, esecuzione o in ogni combinazione di queste modalità, r la combinazione vuota. Quando un file viene creato chiamando open (),è possibile utilizzare un intero ottale di 3 cifre come terzo parametro per definire i permessi. ni cifra ottale controlla i permessi in lettura, scrittura ed esecuzione; la prima cifra t 1 controlla i permessi per l'utente, la seconda per il gruppo e la terza per gli altri (cioè chiunque).

Mnemonico

r--w--x rwr-x -wx rwx

Significato di ogni cifra ottale nei permessi dei file Rappresentazione binaria Rappresentazione ottale 100 010 001 110 101 011 111

04 02 01 06 05 03 07

lnput/output e sistema operativo 463

Compattando le tre cifre ottali in un numero si ottengono i permessi di accesso al

filt>. La rappresentazione mnemonica è la più facile da ricordare: il primo, il secondo e il h'rzo gruppo di tre lettere si riferiscono rispettivamente all'utente, al gruppo e agli altri.

Mnemonico rw------rw---- r-rwxr-xr-x rwxrwxrwx

Esempi di permessi di accesso ai file Rappresentazione ottale 0600 0604 0755 0777

l pt•rmessi rwx r -x r- x significano che il proprietario può leggere, scrivere ed eseguire il filt·, il gruppo può leggere ed eseguire il file e gli altri possono leggere ed eseguire il file. In UNIX, i permessi di accesso ai file sono visualizzati in forma mnemonica mediante il romando ls -1. Anche in MS-DOS esistono i permessi di accesso ai file, ma solo per tutti.

11.10

Esecuzione di comandi dall'interno di un programma C

l11 funzione di libreria system() permette di accedere ai comandi del sistema operativo. Sia in MS-DOS che in UNIX, il comando date provoca la visualizzazione sullo schermo della data corrente. Volendo stampare questa informazione dal programma è possibilt• scrivere:

system("date"); l11 stringa passata a system () viene trattata come un comando del sistema operativo. Quando l'istruzione viene eseguita, il controllo passa al sistema operativo, viene eseguiIn il comando e quindi il controllo ritorna al programma. vi è un editor di testo utilizzato comunemente in UNIX; volendo utilizzare vi dall'interno di un programma, per scrivere un file il cui nome è stato dato come parametro 11ulla riga di comando, è possibile scrivere quanto segue:

char

command[MAXSTRING];

sprintf ( command, "vi %s", argv [ 1]); printf("vi on the file %s is coming up ... \n", argv[1]); system(command); lln esempio simile funziona anche in MS-DOS, sostituendo vi con il nome di un altro t•ditor disponibile su tale sistema. Come esempio finale, si supponga di essere stanchi delle lettere maiuscole prodotte dal comando dir sul sistema MS-DOS; è possibile scrivere un programma che utilizzi questo comando e converta l'output in lettere minuscole.

464 Capitolo 11

Nel file lower_case. c: /* Scrive sullo schermo solo lettere minuscole. */ #include #include #include #define

MAXSTRING

100

int main(void) {

char int FILE

command[MAXSTRING], *tmp_filename; c; *ifp;

tmp_filename = tmpnam(NULL); sprintf(command, "dir> %s", tmp_filename); system(command); ifp = fopen(tmp_filename, "r"); while ((c = getc(ifp)) l= EOF) putchar(tolower(c)); remove(tmp_filename); return 0; }

Viene usata innanzi tutto la funzione di libreria tmpnam ( ) per creare un nome di fil temporaneo. Viene poi invocato system () per redirigere l'output del comando dir n l file temporaneo, quindi viene stampato sullo schermo il contenuto del file, n rt n le maiuscole in minuscole, e infine, terminato l'uso del file temporaneo, esso viene rimosso chiamando la funzione di libreria remove ( ) . Per ulteriori dettagli su queste funzioni si consulti l'Appendice A, Paragrafo A 12.

11.11

Utilizzo di "pipe" da un programma C

Per comunicare con il sistema operativo il sistema UNIX fornisce popen () e pelose ( ) · queste funzioni non sono disponibili in MS-DOS. Si supponga di essere stanchi li lettere minuscole prodotte dal comando ls di UNIX.

Nel file upper_case. c: #include #include int main(void) {

in t FILE

c· ' *ifp;

lnput/output e sistema operativo 465

ifp = popen("lsM, .. r .. ); while ((c = getc(ifp)) l= EOF) putchar(toupper(c)); pelose (i fp) ; return 0; }

nprimo parametro di popen ()

è una stringa che viene interpretata come un comando

rk-1 sistema operativo; il secondo parametro è una modalità di apertura di un file, • r • o •w·. Quando la funzione viene chiamata essa crea un canale ("pipe", da cui il nome popen) tra l'ambiente chiamante e il comando che deve essere eseguito; in questo esempio si ha accesso a ciò che viene prodotto dal comando ls. Poiché l'accesso al file puntato da i fp avviene via pipe, non è possibile utilizzare le funzioni di posizionamento sui file; prr t~sempio, rewind (i fp) non è in grado di operare. L'unico accesso possibile ai caratlrri è quello sequenziale. Il file aperto da popen ( ) deve essere chiuso con pelose ( ) . Se la chiusura non è esplicita, essa avviene automaticamente all'uscita del programma.

Variabili di ambiente

11.12

~~in UNIX che in MS-DOS sono disponibili variabili di ambiente; esse possono venire llampate sullo schermo utilizzando il seguente programma:

#include int main(int argc, char •argv[], char •env[]) {

in t

i•J

for (i = 0; env[i] l= NULL; ++i) printf(•%s\n .. , env[i]); return 0; }

lllt•rzo argomento di ma in () è un puntatore a char, che può essere immaginato come un array di puntatori a char o come un array di stringhe; il sistema fornisce le stringhe, lnMit•me allo spazio per memorizzarle. L'ultimo elemento dell'array env è il puntatore NULL. Quanto segue è un esempio di output del programma sul nostro sistema UNIX. HOME=/c/c/blufox/center manifold SHELL=/bin/csh TERM=vt102 USER=blufox

Il nome a sinistra del segno di uguaglianza rappresenta una variabile dell'ambiente, mc·ntre a destra è scritto il relativo valore, che va visto come stringa. Quanto segue è un nc·rnpio di output sul nostro sistema MS-DOS.

466 Capitolo 11

COMSPEC=C:\COMMAND.COM BASE=d:\base INCLUDE=d:\msc\include Il sistema UNIX fornisce un comando per la stampa delle variabili di ambiente, i cui nome dipende dalla shell utilizzata: nel caso della C shell il nome è printenv, ntr nel caso della Bourne shell e in M~DOS il nome è set. L'output prodotto dal man coincide con quanto prodotto dal programma precedentemente presentato. Per convenzione, i nomi delle variabili di ambiente sono di solito scritti in aiu lo. In un programma C è possibile accedere al valore di una variabile di ambiente p sandola alla funzione getenv () come parametro. Quanto segue è un esempio dell utilizzo di getenv (). printf("%s%s\n%s%s\n%s%s\n%s%s\n", " Name: " getenv("NAME"), User: ", getenv("USER"), Shell: ", getenv ( • SHELL") , "Home directory: ", getenv("HOME")); Il prototipo di funzione si trova in stdlib.h; nel caso venga passata come parametro un stringa che non sia una variabile di ambiente viene restituito il puntatore NULL.

11.13

Il compilatore C

Esistono differenti compilatori C, e un sistema operativo può fornirne svariati. N seguente tabella sono elencate solo alcune delle possibilità.

Comando

Quale compilatore C viene richiamato

cc ace

Il compilatore C nativo fornito dal sistema. Una delle prime versioni del compilatore ANSI C di Sun Microsystems. Il compilatore C/C++ di Borland in ambiente integrato. Il compilatore C/C++ di Borland in versione a righe di comando. Il compilatore GNU C di Free Software Foundation. Il compilatore High C di Metaware. Il compilatore Oregon C di Oregon Software. Il compilatore Quick C di Microsoft. Il compilatore Turbo C di Borland in ambiente integrato. Il compilatore Turbo C di Borland in versione a righe di comando.

be bee

gcc hc occ qc te

tec

In questo paragrafo vengono presentate alcune delle opzioni utilizzabili con il cc sui sistemi UNIX. Simili opzioni vengono fomite anche da altri compilatori.

m

lnput/output e sistema operativo 467

Se un programma C è contenuto in un singolo file, si supponga pgm.c, il comando

cc pgm. c Induce il codice C presente in pgm.c in codice oggetto eseguibile, che viene posto nel ftlr a.out (o in pgm.exe nel caso di MS-DOS). Il programma può essere eseguito medianlr il comando a.out. Un successivo comando cc riscrive il contenuto del file a.out. Fornrmlo il comando

cc -o pgm pgm.c

al codice eseguibile viene invece scritto direttamente nel file pgm, senza intaccare il runtenuto di un eventuale file a.out esistente. Il comando cc opera in tre fasi: viene richiamato prima il preprocessore, poi il comP'latore e infine illoader, o linker, che riunisce le varie porzioni di codice per costruire d file eseguibile. L'opzione -c può essere utilizzata per la sola compilazione, cioè per rtc.·hiamare il preprocessore e il compilatore, ma non illoader. Questa opzione è utile nrl raso in cui un programma sia scritto su più file. Si consideri il seguente comando: cc -c main.c file1.c file2.c !lir non vi sono errori, vengono creati file oggetto corrispondenti che terminano con il IUffisso .o. Per creare un file eseguibile è possibile utilizzare sia file .c che file .o. Per ,...mpio, se vi fosse un errore nel file main.c, dopo averlo corretto sarebbe possibile Impartire il comando:

cc -o pgm main.c file1.o file2.o 1.1mpiego dei file .o invece dei file .c riduce il tempo di compilazione. Alcune opzioni utili del compilatore

-c -g -o name -p

-v -D name=def

-E -I dir

-M -0 -S

Compila solamente, generando i file .o corrispondenti. Genera codice utile al debugger. Pone il codice eseguibile ottenuto in name. Genera codice utile al profiler. Opzione "verbose", genera molte informazioni. Pone all'inizio di ogni file .c la riga #def in e name de/. Richiama il preprocessore ma non il compilatore. Cerca i file #include nella directory dir. Crea un makefile. Cerca di ottimizzare il codice. Genera codice assembler nei file .s corrispondenti.

468 Capitolo 11

Il proprio compilatore potrebbe non disporre di tutte queste opzioni, ma potrebbe O· che fornirne altre o utilizzare differenti flag. Di solito i compilatori per il sistema tivo MS-DOS supportano differenti modelli di memoria; si consulti la 1 u relativa al proprio compilatore per un elenco dettagliato delle opzioni. Suggerimento: si provi a utilizzare l'opzione -v; alcuni compilatori forniscono . recchie informazioni che possono risultare molto utili per comprendere i dettagli r 1 ti vi al processo di compilazione.

11.14

Utilizzo del profiler

In UNIX, in presenza dell'opzione -p, il compilatore cc produce del codice in grado ~ contare il numero di chiamate di ciascuna funzione. Il codice eseguibile creato dal pilatore è organizzato in maniera tale che venga richiamata automaticamente la · ozi ne di libreria monitor (), e che venga creato un file di nome mon.out. Tale file i n utilizzato dal comando pro! per generare un profilo dell'esecuzione. A titolo di esempio, si supponga di volere un profilo dell'esecuzione della rou ti quicksort presentata nel Paragrafo 8.5; di seguito è riportata la funzione mai n ( ) .

Nel file main.c: #include #include #include #define

N

50000

quicksort(int *, int *);

void

in t mai n (voi d) {

int a[ N], i; srand(time(NULL)); for (i = 0; i < N; ++i) a[i] = rand() % 10000; quicksort(a, a+ N- 1); for (i= 0; i< N- 1; ++i) if (a[i] > a[i + 1]) { printf("SORTING ERROR - byel\n"); exit(1); }

return 0; }

Per ottenere un profilo d'esecuzione del programma, quest'ultimo viene com il t con l'opzione -p:

cc -p -o quicksort main.c quicksort.c

lnput/output e sistema operativo 489

Successivamente si fornisce il comando:

quicksort Ciò provoca la creazione del file mon.out; infine, per ottenere un profilo dell'esecuzione, 1i impartisce il comando

prof quicksort mediante il quale si ottiene la seguente schennata: %t ime 46.9 16.1 11 . 7 10.8 6.9 6.4 1.4 0.0 0.0 0.0 0.0

cumsecs 7.18 9.64 11.43 13.08 14.13 15.12 15.33 15.33 15.33 15.33 15.33

#ca l l 9931 1 19863

ms/call 0.72 2460.83 0.09

50000 19863

0.02 0.05

1 1 1 1

0.00 0.00 0.00 0.00

na me _partition mai n :=tind_pivot mcount rand :=quicksort _monstartup _gettimeofday _profil - tsrand - ime

Non tutte le funzioni menzionate sono definite dall'utente; alcune di esse, tra le quali _gettimeofday, sono routine di sistema. Un profilo d'esecuzione come questo può esM're molto utile quando si desideri migliorare l'efficienza rispetto al tempo d'esecuzione.

11.15

Librerie

Molti sistemi operativi forniscono un'utility per creare e gestire librerie. In UNIX tale utility, detta "archiver", viene richiamata mediante il comando ar. Nel mondo MS-DOS tale utility è detta "librarian", ed è disponibile come aggiunta; per esempio, per MicroROft essa si chiama lib, mentre per Turbo C essa si chiama tlib. Per convenzione, i file di libreria terminano con il suffisso .a in UNIX e con .lib in MS-DOS. In questo paragrafo viene presentato il caso di UNIX, comunque gli stessi concetti valgono anche negli altri ambienti. In UNIX, la libreria standard C si trova solitamente in /usr/lib/libc.a, ma essa potrebbe anche trovarsi, per intero o solo in parte, anche in altri file. Se si dispone di lJNIX si provi il comando:

ar t /usr/lib/libc.a l..a chiave t viene utilizzata per stampare i titoli, o nomi, dei file della libreria. Sono preM"nti parecchi titoli; per contarli è possibile fornire il comando:

ar t /usr/lib!libc.a l wc

470 Capitolo 11

Il questo modo l'output del comando ar diviene l'input del comando wc, provocando il conteggio delle righe, parole e caratteri (il nome wc sta per "word count"). Non è . l sorprendente il fatto che la libreria standard cresca con il tempo; su un DEC VAX 11/ 780 degli anni '80 la libreria standard conteneva 311 file oggetto, mentre su una r hi na Sun del1990 la libreria standard ne conteneva 498. Su una Sun utilizzata al della scrittura di questo libro il numero di file oggetto è 563. Viene ora mostrato come un programmatore possa creare e utilizzare delle li r rie proprie. Ciò viene spiegato nel contesto della creazione di una "libreria . r n lizzata". Nel Paragrafo 11.6 è stata presentata gfopen (), una versione personalizzata di fopen (). In una directory di nome g_lib sono presenti 11 funzioni di questo tipo, e i tanto in tanto ne vengono aggiunte di nuove. Si tratta di funzioni come gfclose () , gcalloc (), gmalloc (), e così via. Ognuna si trova in un file separato, ma al fine lla costruzione di una libreria esse potrebbero trovarsi in un unico file. Come esempio tali funzioni viene riportato il codice di gcalloc ( ) : #include #include void •gcalloc(int n, unsigned sizeof_something) {

void

*p;

if ((p= calloc(n, sizeof_something)) == NULL) { fprintf(stderr, "\nERROR: calloc() failed - bye. \n\n•); exit(1); }

return p; }

Per creare la libreria è necessario compilare prima i file .c per ottenere i file .o spondenti. Successivamente si impartiscono i due comandi seguenti: ar ruv g_lib.a gfopen.o gfclose.o gcalloc.o ranlib g_lib. a

n i-

...

Le chiavi 1UV nel primo comando significano rispettivamente sostituzione (replace), giomamento (update) e verbose. Nel caso essa non esista, con questo comando i n ti ui creata la libreria g_lib.a, mentre, se la libreria esiste già, i file .o menzionati no quelli già presenti nella libreria. Se qualcuno dei suddetti file .o non si trova n lla libreria, esso viene aggiunto. Il comando ranlib viene utilizzato per organizzare la li r ria in un fonnato comodo per illoader. Si supponga di scrivere ora un programma fonnato da main.c e da altri due file .. Se il programma richiama gfopen () è necessario rendere disponibile la libreria al r pilatore, e a tale fine si utilizza il comando: cc

-o pgm mai n. c file 1. c file2. c g_lib. a

lnput/output e sistema operativo 471

Nel caso in cui il programma richiami una funzione senza fornirne la definizione, essa viene cercata prima in g_lib.a e poi nella libreria standard. Nel file eseguibile vengono poste solo le funzioni necessarie. Scrivendo molti programmi formati da più file, ciascun programma viene posto in una directory propria. Inoltre dovrebbe essere disponibile una directory separata per le librerie, come g_lib.a, e un'altra directory per i file d'intestazione associati, come g_lib.h. l prototipi delle funzioni in g_lib.a vengono posti in g_lib.h; questo file d'intestazione viPne incluso ove necessario. Tutto questo materiale può essere gestito mediante l'utility make, discussa nel Paragrafo 11.17.

11.16

Cronometrare il codice C

Molti sistemi operativi permettono l'accesso all'orologio interno della macchina sottoalante; in questo paragrafo vengono presentate alcune funzioni per misurare i tempi. Poiché si intende utilizzare tali funzioni in svariati programmi, esse vengono poste nella libreria di utility u_lib.a. Nel Paragrafo 11.17 viene presentato un programma che utilizza funzioni di entrambe le librerie, g_lib.a e u_lib.a. In ANSI C è possibile accedere all'orologio della macchina tramite alcune funzioni l c:ui prototipi si trovano in time.h. Questo file d'intestazione contiene anche altri costrutti, tra cui le definizioni dei tipi clock_t e t ime_ t, utili per trattare i tempi. Normalmenlt• le due definizioni di tipo sono date da: typedef typedef

long long

clock_t; time_t;

Questi tipi vengono utilizzati a loro volta nei prototipi di funzione. Di seguito sono elenrati i prototipi di tre funzioni utilizzate nelle successive routine di misurazione dei tempi. clock t time t double

clock (voi d) ; time(time_t *p); difftime(time_t time1, time_t time0);

Quando viene eseguito un programma, il sistema operativo tiene traccia del tempo di utilizzo del processare. La funzione clock () restituisce come valore la migliore approsaimazione del sistema relativa al tempo utilizzato dal programma sino a quel punto. Le unità di dock possono variare a seconda della macchina. La macro #define

CLOCKS_PER_SEC

60

l* dipendente dalla macchina */

r definita in time.h, e può venire utilizzata per trasformare in secondi il valore restituito da clock (). Attenzione: nelle versioni preliminari di ANSI C la macro si chiamava

CLK_ TCK.

472

Capitolo 11

La funzione t ime ( ) restituisce il numero di secondi trascorsi dal l o gennaio 1970; sono possibili anche altre unità e altre date di partenza. Se il parametro puntatore passato a t ime ( ) non è NULL il valore restituito viene assegnato anche alla variabile puntata. Quanto segue è un tipico utilizzo della funzione

srand(time(NULL)); che inizializza il generatore di numeri casuali. Se due valori prodotti da t ime ( ) vengono passati a di fft ime (),il valore restituito, di tipo double, rappresenta la differenza espressa in secondi. Viene presentato ora un insieme di routine di misurazione dei tempi utilizzabili per svariati scopi, tra cui lo sviluppo di codice efficiente. Queste routine vengono scritte nel file time_keeper.c. #include #include #include #define

MAXSTRING

100

typedef struct { clock t begin_clock, save_clock; time_t begin_time, save_time; } time_keeper; static time_keeper

tk;

l* nota solo in questo file */

void start time(void) { tk.begin_clock = tk.save_clock = clock(); tk.begin_time = tk.save_time = time(NULL); }

double prn_time(void) {

char in t double

s1[MAXSTRING], s2[MAXSTRING]; field_width, n1, n2; clocks_per_second = (double) CLOCKS_PER_SEC, user_time, real_time;

user_time = (clock() - tk.save_clock) l clocks_per_second; real time = difftime(time(NULL), tk.save time); tk.save clock = clock(); tk.save=time = time(NULL); l* stampa in modo chiaro i valori trovati *l

n1 = sprintf(s1, "%.1f", user_time); n2 = sprintf(s2, "%.1f", real_time); field_width = (n1 > n2) ? n1 : n2;

lnput/output e sistema operativo 473

printf("%s%*.1f%s\n%s%*.1f%s\n\n•, "User time: ", field_width, user_time, " seconds", "Real time: ", field_width, real_time, " seconds"); return user_time; }

Si noti che la struttura tk è esterna alle funzioni ed è nota solo in questo file. Essa viene utilizzata a scopo di comunicazione tra le funzioni. Chiamando start_time( ), i valori restituiti da clock () e t ime () vengono posti in tk. Chiamando prn_ t ime ().vengono utilizzati nuovi valori di clock () e t ime () per calcolare e stampare il tempo dell'utente t• il tempo reale trascorsi, e in t k vengono memorizzati nuovi valori. Il tempo dell'utente è il tempo che il sistema riserva all'esecuzione del programma, mentre il tempo reale è quello misurato effettivamente da un orologio; su un sistema time-sharing essi possono risultare differenti. Nel file si trova anche la funzione prn_ total_t ime (),che non è stata presentata; essa è simile a p rn _t ime ( ) , salvo che i tempi trascorsi vengono calcolati rispetto all'ultima chiamata di start_ t ime () anziché rispetto all'ultima chiamata di una qualunque delle tre funzioni. Poiché queste routine di misurazione verranno utilizzate in svariati programmi, t•sse vengono poste nella libreria di utility u_lib.a mediante i seguenti comandi:

cc -c time_keeper.c; ar ruv u_lib.a time_keeper.o; ranlib u_lib. a Nel file d'intestazione u_lib.h vengono posti i prototipi delle funzioni in u_lib.a; è poi possibile includere questo file d'intestazione ove necessario. Viene ora mostrato come possano essere utilizzate queste routine. In certe applicazioni è desiderabile una veloce moltiplicazione in virgola mobile; per decidere se sia meglio utilizzare variabili f lo a t o double è possibile fare uso del seguente programma.

Nel file mult_time.c: l* Confronta i tempi della moltiplicazione tra float e double. *l

#include #include "u_lib.h" #define

100000000

N

l* cento milioni *l

int main(void) {

long float double

i;

a, b

x,

y

= 3.333,

c = 5.555; 3.333, z = 5.555;

l* valori arbitrari */

474 Capitolo 11

printf("Number of multiplies: %d\n\n", N); printf("Type float:\n\n"); start t ime (); for (I = 0; i < N; ++i) a = b * c; prn_time(); printf("Type double:\n\n"); for (i = 0; i < N; ++i) x = y * z; prn_time(); return 0; }

Su una vecchia macchina con un compilatore C tradizionale, si è sorprendentemente scoperto che la moltiplicazione in singola precisione è più lenta di quella in doppia precisione! Nel C tradizionale ogni f lo a t viene automaticamente promosso a double; forse il risultato dipende dal tempo consumato da tale conversione. Su un'altra macchina con un compilatore ANSI C, si è trovato, come si prevedeva, che la moltiplicazione in singola precisione è più veloce di circa il 30%. Eseguendo il programma su una Sun Sparcstation 10 si è ottenuto il seguente risultato: Number of multiplies: 100000000 Type float: User time: 33.6 seconds Real time: 34.0 seconds Type double: User time: 33.5 seconds Real time: 33.0 seconds Anch'esso è sorprendente! Ci si aspettava che la moltiplicazione di f lo a t rispanniasse circa il30% del tempo, ma così non è. Si osservi che il tempo reale indicato per i double è inferiore al tempo dell'utente; ciò è dovuto alle approssimazioni nel metodo di cronometraggio. Attenzione: per ottenere misure accurate del tempo di moltiplicazione della macchina sarebbe necessario stimare il costo generale in termini di tempo del programma (relativamente trascurabile) e il tempo consumato dai cicli for.

11.17

Utilizzo di make

Mantenere in un unico file un programma di dimensioni medio-grandi che debba essere ricompilato ripetutamente risulta inefficiente e costoso sia per il programmatore che per la macchina. Una strategia migliore consiste nel suddividere il programma in più file .c, da compilarsi separatamente quando necessario. L'utility make può essere utilizzata per tenere traccia dei file sorgente e per fornire un opportuno accesso alle librerie e ai file d'intestazione a esse associate. Questa potente utility è sempre disponibile sul

lnput/output e sistema operativo 475

sistema UNIX, e come aggiunta lo è spesso in MS.DOS; il suo utilizzo facilita sia la costruzione che la manutenzione dei programmi. Si supponga di scrivere un programma formato da alcuni file .h e .c: in genere questi file vengono posti in una directory a essi riservata. Il comando make legge un file, il cui nome di default è makefile, contenente una descrizione delle dipendenze tra i vari moduli, o file, che costituiscono il programma, insieme alle azioni da svolgere. In particolare, esso contiene le istruzioni per compilare e ricompilare il programma. Un file di questo tipo viene chiamato makefile. Per semplicità, si consideri un programma contenuto in due file di nome main.c e sum.c, ciascuno dei quali include un file d'intestazione di nome sum.h; si vuole che il codice eseguibile del programma venga scritto in un file di nome sum. Di seguito è presentato un semplice makefile utilizzabile per lo sviluppo e la manutenzione del prowamma.

sum: main.o sum.o cc -o sum main.o sum.o main.o: main.c sum.h cc -c main.c sum.o: sum.c sum.h cc -c sum.c La prima riga indica che il file sum dipende dai due file oggetto main.o e sum.o: essa è un esempio di riga di dipendenza, e deve iniziare dalla prima colonna. La seconda riga indica come il programma debba essere compilato nel caso sia stato modificato almeno uno dei file .o; essa è una riga d'azione, o di comando. Dopo una riga di dipendenza è possibile specificare anche più di un'azione. Una riga di dipendenza e le righe d'azione che la seguono costituiscono una regola. Attenzione: ogni riga d'azione deve iniziare con un carattere di tabulazione, che sullo schermo appare come una sequenza di spazi. Il comando make esegue per default la prima regola che trova nel makefile; dei file dipendenti in tale regola possono tuttavia essere a loro volta dipendenti da altri file seronda quanto specificato da altre regole, che vengono quindi eseguite prima; tali file possono a loro volta causare l'esecuzione di altre regole. La seconda regola del makefile presentato sopra stabilisce che main.o dipende dai due file main.c e sum.h. Se uno di questi due file viene modificato, la riga d'azione mostra che cosa è necessario fare per aggiornare main.o. Una volta che il makefile è stato l'reato, il programmatore può compilare o ricompilare il programma sum impartendo il romando

make dw fa sì che make legga il file makefile, crei un proprio albero di dipendenze ed esegua lt• azioni necessarie.

476 Capitolo 11

Albero di dipendenze utilizzato internamente da make su m

l

(

sum.o

main.o

(

( main.c

sum.h

main.c

sum.h

Alcune regole, tra cui la dipendenza di ogni file .o dal file .c corrispondente, sono predefinite; il makefile precedente risulta dunque equivalente al seguente: sum: main.o sum.o cc -o sum main.o sum.o main.o: sum.h cc -c main.c sum.o: sum.h cc -c sum.c L'utility make riconosce alcune macro predefinite. Una di esse permette di ottenere un altro makefile equivalente: sum: main.o sum.o cc -o sum main.o sum.o main.o sum.o: sum.h cc -c $*.c In questo caso, la seconda regola stabilisce che i due file .o dipendono da sum.h. Modificando sum.h, sia main.o che sum.o devono essere ricreati. La macro $*.c viene espansa in ma in. c nel caso debba essere creato main.o, mentre viene espansa in su m. c nel caso debba essere creato sum.o. Un makefile è costituito da una sequenza di regole che specificano dipendenze e azioni. Una regola inizia dalla prima colonna con una sequenza di nomi di file "target" separati da spazi, seguita da due punti, seguiti a loro volta da una serie di file che rappresentano i prerequisiti, chiamati anche file sorgente. Tutte le righe successive che iniziano con un carattere di tabulazione rappresentano azioni, come la compilazione, che devono essere svolte dal sistema per aggiornare i file target. I file target dipendono in qualche modo dai file prerequisiti, e devono essere aggiornati quando questi ultimi vengono modificati. Nell'Esercizio 33 del Capitolo 8 è stato proposto un confronto tra qsort () e quicksort (),ed è stato scritto un programma che confronta i tempi d'esecuzione di tre routine d'ordinamento: quicksort (), presentata nel testo, qsort (), fornita dal sistema, e una routine qsort () presente su un altro sistema, della quale era disponibile il

lnput/output e sistema operativo 477

l'odice composto da circa 200 righe contenute in un unico file. Il programma che è stato ottenuto, includendo anche quest'ultimo file, consiste di circa 400 righe suddivise in rinque file. Di seguito è riportato il makefile utilizzato.

Nel file makefile: # Makefile per confrontare routine d'ordinamento.

BASE

lhomelblufoxlbase gcc CFLAGS -O -Wall EFILE $(BASE)Ibinlcompare_sorts INCLS -I$(LOC)Iinclude LIBS $(LOC)Iliblg_lib.a $(LOC)Iliblu lib.a lusrllocal LOC OBJS main.o another_qsort.o chk_order.o compare.o quicksort.o $(EFILE): $(0BJS) ~echo •linking ~$(CC) $(CFLAGS) -o $(EFILE) $(0BJS) $(LIBS) $(0BJS): compare_sorts.h $(CC) $(CFLAGS) $(INCLS) -c $*.c

cc

ANALISI DEL MAKEFILE PER IL PROGRAMMA compare_sorts •

# Makefile per confrontare routine d'ordinamento.

È possibile inserire dei commenti in un makefile; essi iniziano con il carattere# e terminano alla fine della riga. •

BASE lhomelblufoxlbase Questo è un esempio di definizione di macro; la forma generale di una definizione di macro è la seguente:

macro_name = replacement_string Per convenzione, di solito i nomi di macro vengono scritti utilizzando lettere maiuscole, ma ciò non è obbligatorio. La stringa di sostituzione può contenere degli spazi, e un eventuale barra inversa \ alla fine della riga indica la prosecuzione della stringa sulla riga successiva. La home directory utilizzata su questa particolare macchina è l home l blufox, e la sottodirectory base è stata creata per contenere tutte le altre sottodirectory principali: essa può essere intesa come la "base di lavoro". La macro BASE fa riferimento a questa sottodirectory.



= gcc CFLAGS -0 -Wall Le prima macro specifica quale compilatore C venga utilizzato, in questo caso il compilatore GNU C. La seconda macro specifica le opzioni, o "flag C", utilizzate cc

4 78 Capitolo 11

alla chiamata di gcc. L'opzione -0 richiama l'ottimizzatore, mentre l'opzione -Walt richiede la stampa di tutti i messaggi d'avvertimento. Scrivendo invece

cc

gcc

CFLAGS la stringa di sostituzione per CFLAGS sarebbe stata vuota. •

EFILE $(BASE)/bin/compare_sorts La macro EFILE specifica dove deve essere posto il file eseguibile. La valu i n o chiamata, di macro avviene mediante un costrutto della forma $(macro_name ) che produce la stringa, eventualmente vuota, associata a macro_name. La lrin a associata alla macro viene anche detta stringa di sostituzione. Dunque EFILE

=

$(BASE)/bin/compare_sorts

è equivalente a: EFILE

/home/blufox/base/bin/compare_sorts



INCLS LIBS

-I$(LOC)/include $(LOC)/lib/g_lib.a $(LOC)/lib/u_lib.a LOC /usr/local La prima macro specifica l'opzione -I, seguita direttamente dal nome della ir che contiene i file include, la seconda macro specifica due librerie, la terza fica un'altra directory; si noti l'impiego di una barra inversa per proseguire riga successiva. La prima libreria, g_lib.a, è la libreria personalizzata discussa n l Paragrafo 11.15, e la seconda libreria, u_lib.a, è la libreria delle utility discussa n l Paragrafo 11.16. Tali librerie contengono delle funzioni richiamate dal r ram~ ma, e quindi devono essere disponibili per il compilatore. I file d'intestazione ciati,g_lib.h e u_lib.h, si trovano nella directory $(LOC) /include, ed è n ari informare il compilatore che questi file d'intestazione vanno cercati nella u d tta directory. Si noti che una macro può essere valutata prima della sua definizione.



OBJS



$(EFILE): $(0BJS) @echo "linking ... " @$(CC) $(CFLAGS) -o $(EFILE) $(0BJS) $(LIBS) La prima riga è una riga di dipendenza; la seconda e la terza specificano le zi ni da intraprendere. Si presti attenzione al fatto che le righe d'azione iniziano con un

main.o another_qsort.o chk_order.o compare.o quicksort.o La macro OBJS viene definita allo scopo di costituire l'elenco dei file oggetto che i trovano a destra del segno di uguale. Si noti l'impiego della barra inversa per pr seguire sulla riga successiva. Sebbene main.o sia stato scritto per primo, u1 dagli altri in ordine alfabetico, l'ordine è irrilevante.

lnput/output e sistema operativo 479

singolo carattere di tabulazione, che sullo schermo equivale a otto spazi bianchi. Il simbolo@ indica che la linea d'azione stessa non deve essere riscritta sullo schermo (vedi Esercizio 28). Poiché una chiamata di macro ha la forma $ ( macro _n ame)

il costrutto $(EFILE) viene sostituito da

$(BASE)/bin/compare_sorts che a sua volta viene sostituito da:

/home/blufox/base/bin/compare_sorts Analogamente, $ ( OBJS) viene sostituito dall'elenco dei file oggetto, e così via. La riga di dipendenza specifica quindi che il file eseguibile dipende dai file oggetto. Se uno o più file oggetto risultano modificati viene eseguita l'azione specificata. La seconda riga d'azione viene espansa in:

@gcc -O -Wall -o /home/blufox/base/bin/compare_sorts main.o another_qsort.o chk_order.o compare.o quicksort.o /home/blufox/base/lib/g_lib.a /home/blufox/base/lib/u_lib.a

\ \ \

Su questa pagina stampata il testo è stato scritto su quattro righe a causa dello spazio limitato, ma nella realtà esso viene generato come singola riga. Suggerimento: utilizzando per la prima volta make è opportuno evitare l'uso del simbolo@; dopo averne compreso il comportamento esso può essere utilizzato per evitare la stampa sullo schermo dei comandi durante l'esecuzione degli stessi.



$(0BJS): compare_sorts.h $(CC) $(CFLAGS) $(INCLS) -c $*.c La prima riga è una riga di dipendenza, che asserisce che i file oggetto dipendono dal file d'intestazione compare_sorts.h. Se il file d'intestazione è stato aggiornato è necessario aggiornare anche tutti i file oggetto, e tale compito viene eseguito mediante la riga d'azione. In UNIX una riga d'azione deve iniziare con un tab, in M~ DOS essa può anche iniziare con uno o più spazi. Il costrutto$* nella riga d'azione è una macro predefinita detta macro del nome del file di base. Essa viene espansa con il nome del file in corso di creazione, escludendone le estensioni. Per esempio, creando main.o, $*.c viene espansa in main.c, e la riga d'azione diviene:

gcc

-0 -Wall

-1/usr/local/include

-c

main.c

Alcune dipendenze sono predefinite nell'utility make; per esempio, ogni file .o dipende dal file .c corrispondente: ciò significa che se il file .c viene modificato esso viene ricompilato per produrre un nuovo file .o; a causa di ciò occorre richiamare il linker per ricollegare tutti i file oggetto.

480

Capitolo 11



-1/usr/local/include Un'opzione della forma -Idirspecifica una ricerca dei file #include nella ir dir, tale opzione completa l'impiego delle librerie. All'inizio dei file .c che costituiscono il programma si trova la riga

t ry

#include "compare_sorts.h" e all'inizio di compare_sort.h vi sono le seguenti righe:

#include •g_lib.h" #include "u_lib.h" Questi file d'intestazione contengono i prototipi delle funzioni delle librerie h sono state costruite, e l'opzione -1 indica al compilatore dove si trovano tali file. L'utility make può essere utilizzata per mantenere programmi scritti in un qu l iasi linguaggio, non solamente in C e C++; più in generale, make può essere utilizzata r ogni tipo di progetto composto di file con dipendenze e azioni associate.

11.18

Utilizzo di touch

L'utility touch è disponibile in UNIX e spesso anche in MS-DOS, e modifica la data di aggiornamento di un file. L'utility make decide quali azioni intraprendere nfr n le date di aggiornamento dei file, quindi touch può essere utilizzata per forzare il portamento di make. Per presentare l'uso di touch si consideri il makefile discusso nel paragrafo r dente, con i relativi file .h, .c e .o. È possibile associare al file compare_sorts.h la ata corrente fornendo il comando:

touch compare_sorts.h Tale azione associa al file una data di aggiornamento più recente di quella di tutti i fi l oggetto che dipendono da esso; se a questo punto viene impartito il comando

make tutti i file .c vengono ricompilati e collegati per creare un nuovo file eseguibile.

11.19

Altri strumenti utili

Il sistema operativo fornisce parecchi strumenti utili al programmatore, alcuni dei uali presenti su UNIX, sono riportati nel seguente elenco. Utility analoghe sono talvolta disponibili anche in MS-DOS.

lnput/output e sistema operativo 481

Comando

Note

cb

"Abbellitore" per codice C: viene utilizzato per migliorare la stampa del codice C. Debugger a livello di sorgente; è necessario compilare il codice con l'opzione -g. Stampa le righe in cui differiscono due file. Debugger GNU; è necessario compilare il codice con l'opzione -g. Cerca un pattem in uno o più file; uno dei più importanti strumenti per i programmatori. Formattatore di codice C con molte opzioni. Conta le linee, le parole e i caratteri in uno o più file.

dbx

diff gdb

grep indent

wc

L'utility cb legge da stdin e scrive su stdout. Essa non è molto potente; per vedeme il comportamento si impartisca il comando cb < pgm.c

dove pgm.c sia un file non ben fonnattato. L'utility indent è più potente, ma a differenza di cb non sempre è disponibile; per poterla utilizzare in modo efficiente è opportuno leggeme il manuale in linea. Un debugger pennette al programmatore di eseguire una riga di codice alla volta e verificare i valori delle variabili e delle espressioni dopo ogni passo. Ciò risulta estremamente utile per scoprire perché il comportamento di un programma sia differente da quello che il programmatore si aspetta. Esistono molti debugger, e dbx non è uno dei migliori, tuttavia esso è disponibile sulla grande maggioranza dei sistemi UNIX. In MS-DOS i debugger sono prodotti separati da aggiungere al sistema; Borland, Microsoft e altre case forniscono prodotti eccellenti. Strumenti come diff, grep e wc sono di natura generale, e vengono utilizzati da svariati utenti e non solo dai programmatori. Sebbene siano strumenti UNIX, essi sono spesso disponibili anche in MS-DOS, soprattutto grep, molto utile per i programmatori. Infine, si ricordi che il C può essere utilizzato insieme con altri strumenti ad alto livello, alcuni dei quali sono veri e propri linguaggi. Utility

Note

awk bison esh

linguaggio per la ricerca e l'elaborazione di pattem. Versione GNU di yacc. C shell, programmabile. Versione GNU di /ex. Genera codice C per l'analisi lessicale. Versione nuova e più potente di awk. Estrazione di informazioni e rapporti. Editor di linea, riceve i comandi da un file. "Yet another compiler-compiler" (un altro compilatore-compilatore), utilizzato per generare codice C.

/l ex /ex

nawk per/ sed yacc

482 Capitolo 11

Di particolare importanza per i programmatori sono lex e yacc, o le corrispondenti utility GNU flex e bison; vedi il Capitolo 8 di The UN/X Programming Environment di Brian Kernighan e Rob Pike (Englewood Cliffs, N.J.: Prentice-Hall, 1984). La Free Software Foundation produce strumenti GNU, eseguibili su diverse piattaforme e ottenibili via Internet (Internet fornisce accessi remoti ad altri strumenti).

11.20

Riepilogo

l.

Le funzioni printf () e scanf (),e le relative versioni che operano con file e stringhe, utilizzano specifiche di conversione scritte in una stringa di controllo che permette di utilizzare un elenco di parametri di lunghezza variabile.

2.

Il file d'intestazione standard stdio.h viene incluso nel caso vengano utilizzati dei file. Esso contiene le definizioni dell'identificatore FILE (una struttura) e i puntaton ai file stdin, stdout e stderr, oltre ai prototipi di parecchie funzioni per la gestione dei file e alle definizioni delle macro getc () e putc (). La chiamata di funzione getc (i fp) legge il carattere successivo dal file puntato da i fp.

3.

Per aprire e chiudere i file vengono utilizzate rispettivamente fopen () e fclose (); dopo l'apertura di un file i riferimenti a esso avvengono tramite il puntatore al file.

4.

Un file può essere immaginato come una sequenza di caratteri, accessibile sequenzialmente o in modo diretto. Leggendo un carattere da un file, il sistema operativo incrementa di l la posizione dell'indicatore sul file.

5.

All'inizio dell'esecuzione di un programma il sistema apre tre file standard, stdin, stdout e stderr. La funzione printf () scrive su stdout, mentre la funzione scanf () legge da stdin. I file stdout e stderr sono di solito associati allo schermo, il file stdin è associato alla tastiera. Tali associazioni possono essere modificate utilizzando le redirezioni.

6.

I file sono una risorsa limitata; il massimo numero di file che possono essere aperti contemporaneamente è dato dalla costante simbolica FOPEN_MAX in stdio.h. Questo numero dipende dal sistema, e generalmente esso è compreso tra 20 e 100; è compito del programmatore tenere traccia di quali file siano aperti. All'uscita dal programma tutti i file aperti vengono chiusi automaticamente dal sistema.

7. Anche se non sono parte di ANSI C, alcune funzioni che utilizzano i descrittori dei file risultano disponibili su molti sistemi. Esse richiedono buffer definiti dall'utente. I descrittori dei file stdin, stdout e stderr sono rispettivamente O, l e 2. 8.

È possibile eseguire un comando del sistema operativo dall'interno di un programma mediante la chiamata system ().

lnput/output e sistema operativo 483

In MS-DOS l'istruzione system( "dir");

provoca la stampa sullo schermo di un elenco di directory e file. 9.

In UNIX è possibile utilizzare la funzione popen ( ) per comunicare con il sistema operativo. Si consideri il seguente comando: wc *.c

Esso stampa sullo schermo il numero di parole di ogni file .c presente nella directory corrente. Per accedere a questa sequenza di caratteri dall'interno di un programma, il programmatore può scrivere: FILE *ifp; ifp = popen("wc *.c", "r");

È necessario chiudere con pelose () i file aperti con popen ( ) . 10.

Molti sistemi operativi forniscono un'utility per creare e gestire librerie. In UNIX l'utility è chiamata archiver, e viene richiamata con il comando ar. In MS-DOS questa utility, chiamata librarian, è fornita come prodotto da aggiungere al sistema.

Il.

L'utility make può essere utilizzata per tenere traccia dei file sorgente e fornire l'accesso corretto alle librerie e ai file d'intestazione associati.

11.21 l.

Esercizi

Riscrivete il programma dbl_space del Paragrafo 11.5 in modo che esso riceva il nome del file di input come parametro sulla riga di comando e scriva su stdout. Fatto ciò, risulta possibile utilizzare il comando dbl_sp infile > outfile

per raddoppiare la spaziatura del contenuto di qualunque cosa si trovi in injile, stampando l'output su outjile. Poiché il programma è pensato per utilizzare le redirezioni, è sensato utilizzare fprintf ( stderr, ... ) invece di printf ( .•. ) in prn_info (). Se su stdout viene scritto un messaggio d'errore, esso risulterà rediretto e l'utente non lo vedrà sullo schermo. Il simbolo > viene utilizzato per redirigere tutto ciò che viene scritto su stdout, ma non ha alcun effetto su ciò che viene scritto su stderr. Scrivete il programma in due modi, cioè con i messaggi d'errore scritti prima su stderr e poi su stdout. Provate le due versioni del programma per comprenderne i diversi effetti.

484 Capitolo 11

2.

Riscrivete il programma dbl_space del Paragrafo 11.5 in modo che utilizzi un'opzione sulla riga di comando come -n, dove n può essere l, 2 o 3. Se n è ll'output deve avere la spaziatura singola, vale a dire che se esistono due o più newline consecutivi nel file di input essi devono essere sostituiti da un unico newline in quello di output. Se n è 2 il file di output deve avere spaziatura esattamente doppia, cioè uno o più newline contigui nel file di input devono essere sostituiti da una coppia di newline nel file di output. Se n è 3 il file di output deve avere spaziatura esattamente tripla.

3.

Scrivete le funzioni getst ring () e putstring (). La prima funzione deve utilizzare un puntatore a file, per esempio i fp, e la macro getc () per leggere una stringa dal file puntato da i fp. La seconda funzione deve utilizzare un puntatore a file, per esempio ofp, e la macro putc () per scrivere una sbinga sul file puntato da of p. Scrivete un programma per provare le vostre funzioni.

4.

Scrivete un programma per numerare le righe in un file. Il nome del file di input deve essere passato al programma come parametro sulla riga di comando, e il programma deve scrivere su stdout. Ogni riga del file di input deve essere scritta sul file di output preceduta dal relativo numero e da uno spazio.

5.

Nell'Appendice A. Paragrafo A12, viene descritta, fra l'altro, la funzione ungete (); dite se, dopo che tre caratteri sono stati letti da un file, è possibile riporre i tre caratteri nel file mediante ungete (). Scrivete un programma di prova.

6.

Scrivete un programma che stampi sullo schermo 20 righe di un file alla volta. nome del file di input deve essere dato come parametro sulla riga di comando. programma deve stampare le 20 righe successive dopo che è stato premuto il tasto retum; si tratta di una versione elementare dell'utility more di UNIX.

7.

Modificate il programma scritto per l'Esercizio 6 facendo sì che esso stampi uno o più file dati come paramebi sulla riga di comando, permettendo inoltre l'utilizzo di un'opzione -n, dove n rappresenta un intero positivo che specifica il numero di righe da stampare ogni volta. In MS..DOS il comando per cancellare lo schermo si chiama cis; in UNIX clear. Provate uno di questi comandi sul vostro sistema per comprenderne il comportamento, e utilizzate system ( "cls") o system ( "clear •) nel vostro programma prima di stampare sullo schermo ciascun gruppo di righe.

8.

La funzione di libreria fgets () può venire utilizzata per leggere da un file una riga alla volta. Consultate la parte relativa a questa funzione nell'Appendice A. Paragrafo A.12, poi scrivete un programma di nome search per la ricerca di pattem. comando

n n

n

search hello my_file deve far sì che la stringa hel/o venga ricercata nel file my_fi/e, e che ogni riga contenente il pattem venga stampata; si tratta di una versione elementare di grep. Suggerimento: utilizzate il seguente codice.

lnput/output e sistema operativo 485

char line[MAXLINE], *pattern; FILE *ifp; if (argc l= 3) { }

if ( (ifp = fopen(argv[2], "r")) == NULL) { fprintf(stderr, "\nCannot open %s\n\n", argv[2]); exit(1); }

pattern = argv[1]; while (fgets(line, MAXLINE, ifp) l= NULL) { if (strstr(line, pattern) l= NULL) 9. l O.

Modificate la funzione scritta per l'Esercizio 8 in modo che in presenza dell'opzione -n venga stampato anche il numero di riga. Compilate il seguente programma e ponetene il codice eseguibile in un file di nome

try_me: #include in t mai n (voi d) {

fprintf ( stdout, "She sells se a shells \n •); fprintf(stderr, "by the seashore. \n"); return 0; }

Eseguite il programma per comprenderne il funzionamento. Cosa succede redirigendo l'output? Provate il comando try_me > tmp

ed esaminate poi il contenuto del file tmp. In UNIX dovreste provare anche il comando: try_me > & tmp

Ciò fa sì che anche l'output scritto su stderr venga rediretto. Esaminate il contenuto di tmp, potreste restame sorpresi! Il.

Scrivete un programma di nome wrt_rand che crei un file di numeri uniformemente distribuiti. Il nome del file deve poter essere inserito in maniera interattiva. Il programma dovrebbe utilizzare tre funzioni, e di seguito ne è mostrata la prima. void get_info(char *fname, int *n_ptr) {

printf("\n%s\n\n%s",

486 Capitolo 11

"This program creates a file of random numbers.", "How many random numbers would you like? "); scanf("%d", n_ptr); printf("\nln what file would you like them? "); scanf("%s", fname); }

Dopo che questa funzione è stata chiamata in mai n () potete scrivere: ofp = fopen(fname, "w"); Tuttavia il file specificato potrebbe già esistere, e in questo caso una sovrascrittura provocherebbe la perdita del contenuto corrente. In questo esercizio si richiede di scrivere del codice che eviti questo problema: in caso di esistenza del file l'utente deve esserne informato, e il programma deve chiedere il permesso di riscrivere il file. Utilizzate come seconda funzione del programma la seguente versione "prudente" di fopen (): FILE *cfopen(char *fname, char •mode) {

char FILE

reply [ 2] ; *fp;

if (strcmp(mode, "w") == 0 && (access(fname, F_OK) -- 0) { printf("\nFile exists. Overwrite it? "); scanf("%1s•, reply); if (*reply l= 'y' && •reply l= 'Y') { printf("\nByel\n\n"); exit(1); } }

fp gfopen(fname, mode); return fp; }

Consultate nel Paragrafo A 16 dell'Appendice A la parte riguardante access ( ) . La terza funzione è gfopen (), la versione personalizzata di fopen () presentata nel Paragrafo 11.6. Suggerimento: per scrivere in maniera ordinata i numeri uniformemente distribuiti utilizzate il seguente codice: for (i= 1; i tmp; 1s -1 tmp chmod a+rwx tmp; 1s -1 tmp chmod 0660 tmp; 1s -1 tmp chmod 0707 tmp; 1s -1 tmp

I numeri sono ottali. Gli zeri iniziali sono necessari?

27.

Esaminate con attenzione il profilo d'esecuzione presentato nel Paragrafo 11.14. Potete osservare che rand ( ) viene chiamata 50.000 volte. Questo è corretto in quanto la dimensione dell'array è 50.000. Osservate che il numero di chiamate della funzione find_pivot ()coincide con il numero di chiamate di quicksort ( ). Osservando il codice potete facilmente convincervi che anche questo è corretto. Potete dare una spiegazione precisa della relazione tra il numero di chiamate di partition() e il numero di chiamate di quicksort( )?

492

Capitolo 11

28.

L'ultimo makefile presentato in questo capitolo è reale; sebbene esso sia stato analizzato in dettaglio, a chiunque non abbia esperienza con make alcuni concetti trebbero risultare difficili da comprendere. Se questa utility vi risulta nuova, pr va te a inserire il comando make dopo aver creato il seguente file:

Nel file makefile: # Esperimento con il comando make. go: hello date list ilecho Goodbyel hello: ilecho Hellol date: ildate; date; date list: ilpwd; ls

Che cosa succede rimuovendo i caratteri il? Che cosa succede se esiste un file nome hello o un file di nome date? 29.

Create il seguente file, e impartite quindi il comando make. Che cosa viene tam pato? Annotate prima la vostra risposta, poi verificatela sperimentalmente.

Nel file makefile: # Esperimento con il comando makel Start: A1 A2 ilecho Start A1: A3 ilecho A1 A2: A3 ilecho A2 A3: ilecho A3

30.

d~

Che cosa viene stampato?

#include int main(void) {

printf("Hellol\n"); fclose(stdout); printf("Goodbyel\n"); return 0; }

Capitolo 12

Applicazioni avanzate

Il C permette di lavorare a un livello vicino a quello della macchina. Utilizzato in origine pt·r implementare UNIX, trova tuttora largo impiego nella costruzione di sistemi. In questo capitolo vengono descritte alcune applicazioni avanzate, presentando anche malt·riale utile ai programmatori di sistema. Viene inoltre discusso l'utilizzo delle matrici 1x·r calcoli matematici.

Creazione di un processo concorrente mediante fork()

12.1

t l NIX è un sistema operativo multiutente e multiprocesso. Ogni processo ha un numero idPntificativo unico (PIO). Il seguente comando serve per mostrare che cosa stia ese.cuendo la macchina, ps r

-aux

di seguito è riportato un esempio del suo output:

USER blufox amber

root

PIO %CPU %MEM 17725 34.0 17662 1.4 143 0.5

SZ RSS TT STAT START

1.6 146 105 i2 R 7.0 636 469 j5 S 0.1 5 3? S

TIME

COMMAND

15:13 0:00 ps -aux 15:10 0:08 vi find it.c Jul 31 10:17 /etc/update

l.u prima riga contiene le intestazioni, le altre forniscono informazioni su ciascun pr~ n·sso: il nome di login dell'utente, il numero identificativo del processo e così via (per nmJ{giori informazioni sul comando ps, consultate il manuale in linea). Il sistema operalivo esegue concorrentemente molti processi suddividendo il tempo delle risorse macrhina (ti me sharing).

494 Capitolo 12

In UNIX, il programmatore può utilizzare fork () per creare un nuovo processo, detto processo figlio, che operi concorrentemente con il processo padre (tale funzione non fa parte di ANSI C). La funzione fork () non riceve alcun parametro e restituisce un int. n semplice programma seguente ne mostra l'utilizzo: #include int main(void) {

int

fork(void), value;

value = fork(); printf("In main: value return 0;

= %d\n",

/*processo nuovo*/ value);

}

A ogni esecuzione, l'output di questo programma risulta differente. Eccone un esempio: In main: value In main: value

= 17219 = 0

Come osservato, quando fork () viene chiamata crea un nuovo processo, il processo figlio. Esso è una copia esatta del processo padre, eccetto per il numero identificativo. La chiamata di funzione fork ( ) restituisce O al processo figlio e restituisce l'identificatore di processo (PID) del figlio al processo padre. Nell'esempio di output precedente, la prima riga è stata stampata dal processo padre e la seconda dal figlio. Si consideri ora lo stesso programma, modificato aggiungendo al codice una seconda copia dell'istruzione value

= fork();

Ecco un esempio di output: In In In In

main: main: main: main:

value value value value

17394 0 0

17395

Si osservi che i figli hanno numeri identificativi unici. Il programma ha creato quattro versioni concorrenti di mai n (). L'ordine d'esecuzione di questi processi dipende dal sistema ed è nondeterministico, non è cioè necessariamente lo stesso a ogni esecuzione del programma (vedi Esercizio l). Attenzione: troppe chiamate a fork ( ) possono provocare un errore di sistema, esaurendo tutti i processi disponibili. Quando viene chiamata, fork ( ) crea due processi, ognuno dei quali ha un proprio insieme di variabili; se tra esse vi è un puntatore a file occorre prestare attenzione perché entrambi i processi condivideranno lo stesso file. n valore restituito da fork () può essere utilizzato in un'istruzione i f- else per scegliere tra le azioni del padre e del figlio. Il programma successivo calcola i numeri di

Applicazioni avanzate 495

Fibonacci nel processo figlio e stampa nel processo padre il tempo trascorso. Viene utilizzata sleep () per sospendere l'esecuzione del processo padre per intervalli di 2 secondi. l* Calcola i numeri di Fibonacci e stampa asincronicamente il tempo. */

#include #include int int void

fib( int); fork(void); sleep(unsigned);

int main(void) {

int

begin = time(NULL), i;

if (fork() == 0) /*figlio */ for (i = 0; i < 30; ++i) printf( "fib(%2d) = %d\n", i, fib(i)); else /* padre */ for (i = 0; i < 30; ++i) { sleep(2); printf("elapsed time = %d\n", time(NULL) - begin); }

return 0; }

in t f ib (in t n) {

i f (n > >>

=0

Input an integer: 3 Value being returned: 3

Queste righe sono generate dal programma try_me. Al prompt è stato inserito un 3 da tastiera, e ciò è riportato nella seconda riga .



After try_me: status

= 0

A prima vista questo risultato può sorprendere, ma occorre ricordare che il valore di status cambia a ogni comando. Pertanto il valore di status scritto in questo caso è quello corrispondente al comando echo che ha stampato - - -, ed è quindi O e non 3. •

>> >>

Input an integer: 7 Value being returned: 7

After try_me again: val = 7 Dopo la nuova chiamata di try_me viene inserito 7. Subito dopo l'uscita da try_me, la variabile di shell val viene utilizzata per ricordare il valore corrente $status. Nello script di shell le righe corrispondenti sono:

try_me set val = $status e c ho echo After try_me again: val

= $val

Il ciclo while nello script di shellgo_try permette all'utente di effettuare degli esperimenti. È possibile, per esempio, osservare come inserendo un valore intero n il valore stampato sia n mod 256, e in particolare come -l produca 255. Lo stato di un programma è quindi un intero compreso tra O e 255. Quando mai n ( ) restituisce un valore alla shell, essa può utilizzarlo per propri scopi. Sebbene in questo paragrafo la discussione sia stata svolta riferendosi alla C shell di UNIX, essa può essere estesa in maniera simile alle altre shell di UNIX e a MS.DOS.

520 Capitolo 12

12.8

Riepilogo

l.

Un processo concorrente è costituito da codice che viene eseguito simultaneamente al codice che lo ha richiamato. In UNIX, mediante fork () è possibile creare un processo figlio che è una copia del processo padre, salvo che il figlio ha un numero identificativo di processo proprio e unico. Se la chiamata a fork () ha successo, al figlio viene restituito Oe al padre il numero identificativo del processo figlio; in caso di insuccesso viene restituito -l.

2.

Sia in MS.DOS che in UNIX, è possibile sostituire un processo con un altro utilizzando una funzione della famiglia ex e c ... ();non c'è ritorno al processo padre. In MS.DOS è disponibile la famiglia spawn ... (),con la quale è possibile ritornare al padre; il suo impiego è paragonabile, in un certo senso, all'utilizzo combinato di fork () ed exec ... () in UNIX.

3.

In UNIX, la chiamata di sistema pipe ( pd), dove pd è un array bidimensionale di in t, crea un meccanismo per la comunicazione tra processi chiamato "pipe". Dopo che una pipe è stata aperta, mediante delle chiamate a fork () è possibile creare processi che comunicano tramite essa utilizzando le funzioni re ad () e wri te ().

4.

Nella libreria standard è disponibile la funzione signal (),che pennette di associare a un segnale un gestore di segnale. Il gestore del segnale può essere una funzione scritta dal programmatore in sostituzione dell'azione di default del sistema. In presenza del segnale, il controllo del programma passa al gestore del segnale. L'insieme di segnali gestiti dal sistema operativo è definito come macro in signa/. h. Questo insieme dipende dal sistema, ma alcuni segnali sono presenti sia in MS. DOS che in UNIX.

5.

Il problema dei cinque filosofi è un modello standard di sincronizzazione di processi concorrenti che condividono risorse. Il problema consiste nello scrivere un programma, con processi concorrenti che rappresentano i filosofi, in cui ogni filosofo riesca a mangiare, cioè a utilizzare risorse condivise, abbastanza spesso.

6.

Un semaforo è una particolare variabile che pennette operazioni di wait e signa/. La variabile è una locazione particolare per memorizzare valori non specificati. L'operazione di wait attende un oggetto e lo rimuove; l'operazione di signa} aggiunge un oggetto. L'operazione di wait blocca un processo fino a quando esso può operare la rimozione; l'operazione di signa} pennette la ripresa di un processo eventualmente bloccato.

7.

È possibile costruire matrici di qualunque dimensione a partire dal tipo puntatore a puntatore a double. Tale puntatore può essere passato a funzioni progettate per lavorare con matrici di qualunque dimensione. Questa è un'idea importante per ingegneri e scienziati.

Applicazioni avanzati 5R f

Esercizi

12.9 l.

Modificate il programma presentato nel Paragrafo 12.1 in modo che contenga tre copie della riga:

value = fork(); L'output del programma è nondeterministico. Spiegate che cosa significa. Suggerimento: eseguite il programma più volte. 2.

Se fork () fallisce non viene creato alcun processo figlio e viene restituito -l. Scrivete un programma contenente il seguente codice:

#define

N

3

for (i= 1; i 0) printf("%2d: Hello from parent\n", i); else printf("%2d: ERROR: Fork did not occur\n", i); }

Quanto deve essere grande N sul vostro sistema affinché venga stampato il messaggio d'errore? 3.

Nel Paragrafo 12.2 è stato presentato un programma che mostra come un processo possa sostituime un altro. Modificate tale programma; cominciate col creare un altro file eseguibile di nome pgm3 che stampi la data corrente. Fomite questo programma come ulteriore scelta per la sostituzione del processo padre nel programma che modificate.

4.

Sul vostro sistema è disponibile il comando fortune? Se sì, localizzate il suo codice eseguibile. Prendete poi uno dei vostri programmi e inserite del codice del tipo:

if (fork() == 0) execl( "/usr/games/fortune", "fortune•, 0); Che cosa succede? 5.

Scrivete un programma che utilizzi n processi concorrenti per moltiplicare due matrici n x n.

6.

Compilate due programmi in eseguibili chiamati progl e prog2. Scrivete un programma che li esegua concorrentemente utilizzando due chiamate a fork () e richiamando execl () per sostituire i due eseguibili.

522 Capitolo 12

7.

Qual è la capacità massima di una pipe sul vostro sistema? Scrivete un programma per scoprirlo. Suggerimento: riempite la pipe di caratteri fino a quando sia possibile.

8.

Modificate il programma dei cinque filosofi in modo che, ricevendo un segnale di interrupt, stampi quante volte ciascun filosofo ha mangiato. Provate anche a vedere che cosa succede se la funzione pick_up () viene modificata in modo che il filosofo numero 3 prenda prima il bastoncino destro, mentre tutti gli altri prendono prima il bastoncino sinistro.

9. La gestione dei segnali dipende dal sistema. Provate il seguente programma su sistemi differenti per osservarne i vari comportamenti: #include #include #include

l* contiene la definizione di HUGE_VAL *l

int main(void) {

double

x = HUGE_VAL, y

HUGE_VAL;

signal(SIGFPE, SIG_IGN); printf("Ignore signa!: x* y = %e\n", x* y); signal(SIGFPE, SIG_DFL); printf("Default signa!: x* y = %e\n", x* y); return 0; }

10.

Nel corso di questo capitolo è stato mostrato come, a partire da una variabile di tipo double * *, sia possibile costruire in maniera dinamica una matrice e passarla a funzioni. È proprio necessario che la costruzione. venga fatta in maniera dinamica? Considerate il seguente codice:

i•

in t double double

' *a(3], det; trace(double **

'

in t);

for (i = 0; i < 3; ++i) a[i] = calloc(3, sizeof(double)); tr

= trace(a,

l* prototipo

di funzione *l

l* riempie

la matrice a *l

3);

L'array a viene passato come parametro, ma dal prototipo di funzione risulta necessario un parametro di tipo double * *. Il vostro compilatore segnala dei problemi? Spiegatene il perché. 11.

Negli anni '70, prima che il C fosse ampiamente disponibile, veniva spesso utilizzato il linguaggio PL/1, diffuso sui mainframe IBM. Nei manuali del PL/1 viene sot-

Applicazioni avanzate 523

tolineato sempre il fatto che il compilatore può creare vettori e matrici che partono da un qualunque indice. Per esempio, con una dichiarazione della forma int

automobiles[1989 : 1999]; /*dichiarazione in stile PL/1 */

è possibile creare un array di lunghezza 11, il cui primo indice è 1989. Questo array potrebbe essere utilizzato per memorizzare il numero di automobili vendute, o che si prevede di vendere, negli anni compresi tra ill989 e ill999. Probabilmente questo concetto veniva sottolineato in quanto il FORTRAN e altri linguaggi non permettevano di creare tali array. In C, naturalmente, è possibile creare un array come questo: spiegate come.

12. A scopo di test può essere utile riempire delle matrici con interi distribuiti in maniera casuale tra -N e +N. Verificando a mano i calcoli della macchina, un valore ragionevole di N è 2 o 3. Scrivete una funzione per riempire delle matrici. void fill_matrix(double ••a, int m, int n, int N) {

13.

Sia A=(a;) una matrice n x n. Il determinante di A può essere calcolato utilizzando l'eliminazione Gaussiana. Questo algoritmo richiede di effettuare le seguenti operazioni, con k compreso tra l e n-l. Partendo da a,. esaminate tutti gli elementi nella relativa colonna che si trovano sotto a,., compreso a,. stesso. Tra questi elementi trovate quello con massimo valore assoluto, che verrà chiamato pivot. Se il pivot si trova sulla riga i, con i diverso da k, le righe i e k si scambiano (tenete traccia del numero di scambi effettuati). Dopo tutto ciò il pivot è quindi a... Se esso vale zero il valore del determinante è zero e l'algoritmo termina; altrimenti, per i compreso tra k+ l e n, è necessario sommare all'i-esima riga un multiplo della k-esima riga, ottenuto moltiplicando ogni elemento per (-1/a.J x a;•· Il passo finale consiste nel calcolare il prodotto degli elementi sulla diagonale della matrice. Se il numero di scambi di righe operati nell'algoritmo è pari, tale prodotto rappresenta il determinante, altrimenti il determinante si ottiene cambiando segno a tale prodotto. Scrivete un programma che calcoli il determinante di una matrice n x n. Utilizzate la funzione get_mat r ix_space () per avere indici che inizino da l anziché da O. Suggerimento: al fine di comprendere i dettagli dell'algoritmo, simulatelo manualmente su una piccola matrice prima di costruire il programma. far (k = 1; k > miles; cout n; assert(cin && n>= 0); for (i = 2; i m))

In questo caso il tipo carattere esteso wchar _t è definito come semplice char. Un sistema può definirlo come un qualsiasi tipo intero. Esso deve essere in grado di mem~ rizzare il più grande insieme esteso di caratteri tra tutti quelli supportati localmente. n tipo prtdi ff _t è il tipo ottenuto sottraendo due puntatori. Il tipo size_ t è il tipo ottenuto utilizzando l'operatore sizeof. Una chiamata di macro della forma offsetof (s_type,

La libreria standard 57e

m) calcola lo spiazzamento in byte del membro mdall'inizio della struttura s_type; nel seguente programma ne viene illustrato l'utilizzo: #include #include typedef struct { double a, b, c; } data; int main(void) {

printf(.%d %d\nM, offsetof(data, a), offsetof(data, b)); return 0; }

Sulla maggior parte dei sistemi l'esecuzione di questo programma produce la stampa di Oe 8.

A.12 lnput/output: In questo file d'intestazione sono contenute macro, definizioni di tipo e prototipi di funzioni utilizzate dal programmatore per accedere ai file. Di seguito sono riportati alcuni esempi di macro e definizioni di tipo: #define #define #define #define #define #define #define tyepdef typedef typedef

BUFSIZ

l* dimensione di ogni buffer di IlO *l EOF ( -1 ) l* valore restituito alla fine del file *l FILENAME_MAX 255 l* lunghezza massima dei nomi dei file *l FOPEN_MAX 20 l* massimo numero di file aperti *l L_tmpman 16 l* lunghezza dell'array per nomi di file temporanei *l NULL 0 l* puntatore nullo *l TMP_MAX 65535 l* numero di nomi differenti generati da tmpnam() *l long pos_t; l* utilizzata con fsetpos() *l unsigned size_t; l* tipo dell'operatore sizeof *l va_list; l* utilizzato con la famiglia char * vfprintf() *l 1024

Una struttura di tipo FILE possiede dei membri che descrivono lo stato corrente di un file. Il nome e il numero dei suoi membri dipendono dal sistema; di seguito è riportato un esempio.

580 Appendice A

typedef struct { in t

cnt;

l* dimensione della parte di buffer inutilizzata *l l* prossima locazione

unsigned char

*b_ptr;

unsigned char in t short

a cui accedere nel buffer */ *base; l* inizio del buffer *l bufsize; l* dimensione del buffer */ flag; l* informazioni memorizzate bit a bit *l fd; l* descrittore di file */

char } FILE;

_iob[];

extern FILE

Un oggetto di tipo FILE deve essere in grado di memorizzare tutte le infonnazioni necessarie a controllare uno stream, tra le quali un indicatore di posizione nel file, un puntatore al relativo buffer, un indicatore d'errore che indica se si è verificato un errore di lettura o scrittura e un indicatore di fine file che indica se è stata raggiunta la fine del file. L'implementazione dipende dal sistema. Per esempio, l'indicatore d'errore e l'indicatore di fine file possono essere codificati tra i bit del membro flag. Generalmente, il tipo fpos_t è definito da quanto segue: typedef

long

fpos_t;

Si suppone che un oggetto di questo tipo sia in grado di memorizzare tutte le infonnazioni necessarie a specificare in maniera univoca ogni posizione nel file. Per definire stdin, stdout e stderr vengono utilizzate delle macro. Sebbene si pensi a essi come a dei file, in realtà si tratta di puntatori: #define #define #define

stdin stdout stderr

(&_iob[0]) (& iob[1]) (&=iob[2])

Contrariamente ad altri file, non è necessario che il programmatore apra esplicitamente stdin, stdout e stderr. Alcune macro sono concepite per essere utilizzate con alcune funzioni: #define

IOFBF

#define

IOLBF

#define

IONBF

#define #define

SEEK SET SEEK=CUR

#define

SEEK_END

l* setvbuf(): bufferizzazione completa *l 0x80 l* setvbuf(): bufferizzazione

0

a linee */ 0x04 l* setvbuf(): nessuna bufferizzazione *l 0 l* fseek () : inizio del file *l 1 l* fseek () : posizione corrente nel file */ 2 l* fseek(): fine del file */

Quando un file viene aperto, il sistema operativo lo associa a uno , . . • ....,,•• ne le informazioni sullo stream in un oggetto di tipo FILE. Un puntatore a 'l LI 1JUb essere visto come associato al file, allo stream o a entrambi.

Apertura, chiusura e condizioni su un file • FILE *fopen(const char *filename, const char •mode); Esegue le operazioni relative all'apertura di un file bufferizzato. Se la chiamata ha successo viene creato uno stream e restituito un puntatore a FILE a esso associato. Se non è possibile accedere a f ilename viene restituito NULL. Le modalità di base per l'apertura dei file sono • r", "w" e "a" che corrispondono rispettivamente a lettura (re ad), scrittura (write) e aggiunta alla fine (append). L'indicatore di posizione nel file è impostato all'inizio se la modalità d'apertura è "r .. o w.. , e alla fine nel caso sia "a ... Se la modalità è .. w.. o .. a" e il file non esiste, esso viene creato. La modalità di aggiornamento (sia lettura che scrittura) viene indicata con +. Un file binario viene indicato con b. Per esempio, la modalità .. r+ • viene utilizzata per aprire un file di testo sia per la lettura che per la scrittura, mentre la modalità .. rb • viene utilizzata per aprire per la lettura un file binario. La modalità "rb+ .. o "r+b • viene impiegata per aprire un file binario sia in lettura che scrittura. Convenzioni simili vengono applicate anche a "w" e • a" (vedi Paragrafo 11.9). In modalità di aggiornamento non è possibile far seguire immediatamente un'operazione di input da una di output, a meno che sia stato raggiunto il marcatore di fine file o sia stata effettuata una chiamata a una delle funzioni di posizionamento nel file fseek (). fsetpos () o rewind ().Analogamente, un'operazione di input non può seguirne immediatamente una di output a meno che sia avvenuta una chiamata a ff lush ( ) , fseek () o a fsetpos () o rewind (). Il



int fclose(FILE *fp); Esegue le operazioni necessarie per svuotare i buffer e chiudere le connessioni al file associato a fp. Se l'operazione ha successo viene restituito zero, se invece si verifica un errore, o se il file è già chiuso, viene restituito EOF. I file aperti rappresentano una risorsa limitata: il numero massimo di file che possono risultare simultaneamente aperti è dato dalla costante FOPEN_MAX. L'efficienza del sistema migliora tenendo aperti solo i file necessari.



int fflush(FILE *fp); Ogni dato presente nel buffer viene trasferito al file. Se la chiamata ha successo viene restituito zero, altrimenti EOF.



FILE *freopen(const char *filename, const char •mode, FILE *fp); Chiude il file corrispondente a fp, apre f ilename nella modalità specificata da mode, associando fp al nuovo file. Se la chiamata ha successo viene restituito fp, albimenti NULL. Questa funzione risulta utile per modificare i file associati a stdin, stdout o stderr.

582 Appendice A



void setbuf(FILE *fp, char *buf); Se fp è diverso da NULL,la chiamata di funzione setbuf ( fp, buf) è equivalente a setvbuf(fp, buf, _IOFBF, BUFSIZ) salvo che non viene restituito alcun valore. Se fp è NULL, la modalità è _IONBF.



int setvbuf(FILE *fp, char *buf, int mode, size_t n); Determina la modalità di bufferizzazione del file corrispondente a fp. La funzione deve essere chiamata dopo che il file è stato aperto, ma prima che venga effettuato un accesso. Le modalità _IOFBF, _IOLBF e _IONBF corrispondono rispettivamente alla bufferizzazione completa, per righe, e all'assenza di bufferizzazione. Se buf non è NULL, viene utilizzato come buffer I'array di lunghezza n puntato da buf, altrimenti il buffer viene fornito dal sistema. Se la chiamata ha successo viene restituito zero. Attenzione: utilizzando come buffer un array di classe di memorizza. zione automatica, il file deve essere chiuso prima dell'uscita dalla funzione.



FILE •tmpfile(void); Apre un file temporaneo in modalità • wb+ • e restituisce un puntato re associato al file; se non è possibile soddisfare tale richiesta viene restituito NULL. Il sistema rimuove il file dopo la chiusura o all'uscita dal programma.



char *tmpnam(char *s); Crea un nome temporaneo unico che viene di solito utilizzato come nome di file. Se s è diverso da NULL il nome viene memorizzato in s, che deve essere almeno di lunghezza L_tmpnam. Se invece s è NULL il sistema fornisce un array di durata statica per memorizzare il nome. Successive chiamate a tmpnam ( ) possono riscrivere questo spazio. In ogni caso viene restituito l'indirizzo base dell'array in cui è memorizzato il nome. Chiamate ripetute a tmpnam ( ) generano almeno TMP _MAX nomi differenti.

Accesso all'Indicatore di posizione nel file Le funzioni illustrate in questo paragrafo vengono utilizzate dal programmatore per accedere direttamente ai file. Le funzioni tradizionalmente impiegate a questo scopo sono fseek (), ftell () e rewind ();ANSI C ha aggiunto fgetpos () e fsetpos (),che in alcune implementazioni possono essere previste per accedere a file troppo grandi per le funzioni tradizionali. Tuttavia, le vecchie versioni di molti compilatori ANSI C non hanno utilizzato questa opportunità. •

int fseek(FILE *fp, long offset, int place); Predispone l'indicatore di posizione per la successiva operazione di input o di output. Per un file binario la posizione è offset byte da place, e il valore di place può essere SEEK_SET, SEEK_CUR o SEEK_END, che corrispondono rispettivamente all'inizio del file, alla posizione corrente e alla fine del file. Se la chiamata ha successo. l'indicatore di fine file viene cancellato e viene restituito zero.

La libreria standard 583



long ftell(FILE *fp); Restituisce il valore corrente dell'indicatore di posizione relativo al file corrispondente a fp. Nel caso di un file binario il valore restituito è il numero di byte dall'inizio del file, nel caso di un file di testo tale valore dipende dal sistema. In ogni caso, il valore può essere successivamente utilizzato da fseek () per riportare l'indicatore di posizione nel file al punto in cui si trovava al momento della chiamata a ftell (). Se la chiamata a ftell () non ha successo viene restituito -1 L, e in errno viene memorizzato un valore dipendente dal sistema.



void rewind(FILE *fp); Pone l'indicatore di posizione nel file all'inizio del file e cancella gli indicatori di fine file e di errore. La chiamata di funzione rewind (fp) è equivalente a (void) fseek(fp, 0L, SEEK_SET) salvo che fseek () cancella solo l'indicatore di fine file.



int fgetpos(FILE *fp, fpos_t *pos); Pone nell'oggetto puntato da pos il valore corrente dell'indicatore di posizione nel file corrispondente a fp. D valore memorizzato può essere utilizzato successivamente da fsetpos () per ripristinare l'indicatore di posizione nel file. In caso di successo viene restituito zero, altrimenti viene restituito un valore diverso da zero e viene posto in errno un valore dipendente dal sistema.



int fsetpos(FILE *fp, const fpos_t *pos); Pone l'indicatore di posizione nel file al valore puntato da pos. In caso di successo l'indicatore di fine file viene cancellato e viene restituito zero, altrimenti viene restituito un valore diverso da zero e viene posto in errno un valore dipendente dal sistema.

Gestione degli errori



void clearerr(FILE *fp); Disattiva gli indicatori di errore e di fine file relativi al file conispondente a fp.



int feof(FILE *fp); Restituisce un valore diverso da zero se l'indicatore di fine file per il file corrispondente a fp risulta attivo.



int ferror(FILE *fp); Restituisce un valore diverso da zero se l'indicatore di errore per il file corrispondente a fp risulta attivo.



void perror(const char *s); Stampa su stderr un messaggio d'errore associato a errno, preceduto dalla stringa s, seguita dai due punti e da uno spazio, e seguito da un newline; la chiamata di funzione strerror(errno) stampa solo il messaggio.

584 Appendice A

lnput/output di caratteri • int getc(FILE *fp); Equivale a fgetc (), salvo che viene implementata come macro. Poiché fp può essere valutato più di una volta nella definizione di macro, una chiamata con un parametro avente effetti collaterali, come per esempio fgetc (*p++), può non funzionare correttamente.



int getchar(void);

La chiamata getchar () è equivalente a getc ( stdin). •

char *gets(char *s); Legge i caratteri da stdin e li memorizza nell'array puntato da s fino a quando vengono raggiunti un newline o la fine del file; a questo punto ogni newline viene ignorato e viene inserito un carattere nullo; al contrario, fgets () mantiene il newline. Se viene scritto almeno un carattere viene restituito s, altrimenti NULL.



int fgetc(FILE *fp); Preleva il carattere successivo dal file corrispondente a fp e ne restituisce il valore. Incontrando la fine del file viene attivato l'indicatore di fine file e viene restituito EOF; in caso di errore viene attivato l'indicatore d'errore e viene restituito EOF.



char *fgets(char *line, int n, FILE *fp); Legge al più n - 1 caratteri dal file corrispondente a fp e li pone nell'array puntato da line. La lettura dei caratteri viene interrotta appena vengono incontrati un newline o la fine del file, e alla fine del processo viene scritto nell'array un carattere nullo. Se la fine del file viene incontrata all'inizio della lettura, il precedente contenuto di l in e non viene modificato e la funzione restituisce NULL; altrimenti restituisce line.



int fputc(int c, FILE *fp); Converte il parametro c al tipo unsigned char, scrivendo il risultato nel file corrispondente a fp. Se la chiamata fputc (c) ha successo viene restituito (int) (unsigned char) c altrimenti viene attivato l'indicatore d'errore e viene restituito EOF.



int fputs(const char •s, FILE *fp); Copia la stringa s, eccetto il carattere nullo di fine stringa, nel file corrispondente a fp; l'analoga funzione puts () aggiunge un newline. Se la chiamata ha successo viene restituito un valore non negativo, altrimenti EOF.



int putc(int c, FILE *fp); È equivalente a fputc (),ma è implementata come macro. Poiché fp può essere valutato più di una volta nella definizione di macro, una chiamata con un parametro avente effetti collaterali, come putc (*p++), può non funzionare correttamente.

La libreria standard 585



int putchar(int c);

La chiamata putchar (c) è equivalente a putc (c, stdout). •

int puts(const char *s); Copia la stringa s sullo standard output, eccetto il carattere nullo di fine stringa, e aggiunge un newline; l'analoga funzione fputs () non aggiunge un newline. Se la chiamata ha successo viene restituito un valore non negativo, altrimenti EOF.



int ungetc(int c, FILE *fp); Reinserisce il valore ( unsigned char) di c nello stream associato a fp, purché il valore di c non sia EOF. È possibile reinserire almeno un carattere, e sulla maggior parte dei sistemi anche di più. I caratteri reinseriti saranno letti dallo stream in ordine inverso rispetto a quello di reinserimento. Una volta letti essi vengono dimenticati, in quanto non vengono posti nel file in modo permanente. Attenzione: una chiamata intermedia a una delle funzioni di posizionamento sui file fseek( }, fsetpos() o rewind() provoca la perdita dei caratteri reinseriti. Inoltre, il funzionamento di ftell () può essere inaffidabile prima della lettura dei caratteri reinseriti.

lnput/output formattato



in t fprintf (FILE *fp, const char *cntrl_st ring, ... ) ; Scrive del testo formattato nel file corrispondente a fp, restituendo il numero di caratteri scritti. In caso di errore viene attivato l'indicatore d'errore e viene restituito un valore negativo. In cntrl_string si possono trovare specifiche di conversione, o formati, che iniziano con un carattere% e terminano con un carattere di conversione. I formati determinano la maniera in cui vengono stampati gli altri parametri; per i dettagli si veda il Paragrafo 11.1.



int printf ( const char •cntrl_string, ... ) ; Una chiamata di funzione della forma printf (cntrl_string, altri_parametri) è equivalente a: fprintf ( stdout, cntrl_string, altri_parametri)



int sprintf ( char *s, const char *cntrl_string, ... ) ; Questa è la versione di printf () che scrive sulla stringa puntata da s anziché su stdout.



int vfprintf(FILE *fp, const char *cntrl_string, va_list ap); int vprintf(const char *cntrl_string, va_list ap); int vsprintf(char s*, const char *cntrl_string, va_list ap); Queste funzioni corrispondono rispettivamente a fprintf (), printf () e sprintf ().Anziché un elenco di parametri di lunghezza variabile, esse utilizzano un puntatore a un array di parametri, come definito in stdarg.h.

586

Appendice A



int fscanf (FILE *fp, const char •cntrl_string, ... ) ; Legge del testo dal file corrispondente a f p e lo elabora secondo le direttive nella stringa di controllo. Vi sono tre tipi di direttive: caratteri normali, spaziature e specifiche di conversione. I caratteri normali vengono associati ai corrispondenti caratteri letti, le spaziature a spaziature opzionali. Una specifica di conversione inizia con un carattere % e termina con un carattere di conversione: essa provoca la lettura di caratteri dall'input stream e il calcolo di un valore corrispondente, che viene posto in memoria all'indirizzo specificato da uno degli albi parametri. Se la funzione viene chiamata e l'input stream è vuoto viene restituito EOF, altrimenti viene restituito il numero di conversioni operate con successo. Per i dettagli si veda il Paragrafo 11.2.



int scanf ( const char cntrl_string, ... ) ; Una chiamata di funzione della forma scanf ( cntrl_string, altri_parametri) è equivalente a: fscanf ( stdin, cntrl_string, altri_parametri)



in t sscanf ( const char •s, const char cntrl_string, ... ) ; Questa è la versione di scanf () che legge dalla stringa puntata da s anziché da stdin. La lettura da una stringa è differente dalla lettura da un file nel senso che se sscanf () viene utilizzata per leggere nuovamente da s, l'input comincia dall'inizio della stringa e non dalla posizione in cui era terminata la lettura precedente.

lnput/output diretto

Le funzioni f rea d ( ) e fwr i t e ( ) vengono utilizzate rispettivamente per leggere e scrivere file binari, senza operare alcuna conversione. In certe applicazioni l'uso di queste funzioni permette un notevole rispannio di tempo.



size_t fread(void •a_ptr, size_t el_size, size_t n, FILE *fp); Legge al più n * el_size byte (caratteri) dal file corrispondente a fp e li pone nell'array puntato da a_pt r. Viene restituito il numero di elementi scritti con successo nell'array. Se viene incontrata la fine del file, viene attivato l'indicatore di fine file e il valore restituito risulta inferiore a quello previsto. Se el_size o n valgono zero, l'input stream non viene letto e viene restituito zero.



size_t fwrite(const void •a_ptr, size_t el_size, size_t n, FILE *fp); Scrive n * el_size byte (caratteri) nel file corrispondente a fp, prelevandoli dall'array puntato da a_pt r. Viene restituito il numero di elementi scritti con successo. In caso di errore, il valore restituito risulta inferiore a quello previsto. Se el_size o n valgono zero l'accesso all'array non avviene, e viene restituito zero.

La libreria standard 587

Rimozione o rlnomlna di un file • int remove(const char *filename); Rimuove dal file system il file di nome filename. Se la chiamata ha successo viene restituito zero, altrimenti -1; nel C tradizionale questa funzione si chiama unlink ().



int rename(const char *from, const char *to); Modifica il nome di un file; il nome vecchio è contenuto nella stringa puntata da f rom, il nome nuovo in quella puntata da t o. Nel caso esista già un file con il nome nuovo, il comportamento dipende dal sistema; in UNIX generalmente il file viene riscritto. Su molti sistemi, i nomi vecchi e nuovi possono rappresentare sia file che directory: se uno dei parametri è il nome di una directory deve esserlo anche l'altro. Se la chiamata ha successo viene restituito zero, altrimenti -1 e un valore dipendente dal sistema viene posto in errno.

A.13 Utility generali: Questo file d'intestazione contiene prototipi di funzioni di uso generale, con relative macro e definizioni di tipo. Ecco alcuni esempi di macro e definizioni di tipo:

l* per size_t e wchar_t *l

#include #define #define #define #define

EXIT SUCCESS EXIT=FAILURE NULL RAND_MAX

0 1

0 32767

typedef struct { quot; in t in t rem; } div_t;

l* quoziente *l l* resto *l

typedef struct { long quot; long rem; } ldiv_t;

l* quoziente *l l* resto *l

l* l* l* l*

utilizzata con exit() *l utilizzata con exit() *l puntatore nullo *l 2"15 - 1 *l

Allocazione dinamica di memoria • void •calloc(size_t n, size_t el_size); Alloca uno spazio di memoria contiguo per un array di n elementi, ognuno dei quali richiede el_size byte. Lo spazio viene inizializzato con tutti i bit a zero. Una chiamata restituisce l'indirizzo base dello spazio allocato in caso di successo, altrimenti NULL.

588 Appendice A



void *malloc(size_t size); Alloca in memoria un blocco di spazio di size byte che non viene inizializzato. Una chiamata restituisce l'indirizzo base dello spazio allocato in caso di successo, altrimenti NULL.



void *realloc(void *ptr, size_t size); Modifica a size byte la dimensione del blocco puntato da ptr. Il contenuto della parte di spazio corrispondente alla minore tra la vecchia e la nuova dimensione non viene modificato, e l'eventuale spazio nuovo non viene inizializzato. Se possibile la funzione non modifica l'indirizzo base del blocco, altrimenti essa alloca un nuovo blocco di memoria, nel quale copia la porzione utile del vecchio, e libera quest'ultimo. Se ptr è NULL l'effetto è lo stesso della chiamata di malloc (). Se pt r è diverso da NULL, esso deve essere l'indirizzo base dello spazio precedentemente allocato da una chiamata a calloc ( ) , malloc () o realloc () che non sia stato ancora deallocato da una chiamata a free () o realloc ().Una chiamata con successo restituisce l'indirizzo base dello spazio ridimensionato (o nuovo), albimenti viene restituito NULL.



void free(void *ptr); Provoca il rilascio dello spazio di memoria puntato da pt r. Se pt r è NULL la funzione non ha alcun effetto, altrimenti pt r deve essere l'indirizzo base dello spazio allocato in precedenza mediante una chiamata a calloc ( ) , malloc ( ) o realloc ( ) che non sia stato ancora deallocato mediante una chiamata a free () o realloc (). Negli altri casi si verifica un errore il cui effetto dipende dal sistema.

Ricerca e ordinamento



void *bsearch(const void *key_ptr, const void *a_ptr, size_t n_els, size_t el_size, int compare(const void *, const void *)); Cerca nell'array ordinato puntato da a_ptr un elemento che coincida con l'oggetto puntato da key _p t r. Se l'elemento viene trovato ne viene restituito l'indirizzo, altrimenti viene restituito NULL. Il numero di elementi dell'array è n_els, e ogni elemento viene rappresentato in el_size byte. Gli elementi dell'array devono essere ordinati in modo crescente rispetto alla funzione di confronto compare (). La funzione di confronto riceve due parametri, ognuno dei quali è l'indirizzo di un elemento dell'array, e restituisce un int che può essere minore, uguale o maggiore di zero a seconda del fatto che l'elemento puntato dal primo parametro sia minore, uguale o maggiore di quello puntato dal secondo; la funzione bsearch () è basata su un algoritmo di ricerca binaria, e ciò ne spiega il nome.



void qsort(void *a_ptr, size_t n_els, size_t el_size, int compare(const void *, const void *)); Ordina l'array puntato da a_pt r in modo crescente rispetto alla funzione di confronto compare ().

La libreria standard 589

Il numero di elementi dell'array è n_els e ogni elemento viene rappresentato in el_size byte. La funzione di confronto riceve due parametri, ognuno dei quali è l'indirizzo di un elemento dell'array, e restituisce un int che può essere minore, uguale o maggiore di zero a seconda del fatto che l'elemento puntato dal primo parametro sia minore, uguale o maggiore di quello puntato dal secondo; la funzione qsort () implementa l'algoritmo di quicksort, e ciò ne spiega il nome. Generatore di numeri pseudocasuall



in t rand (voi d) ; Ogni chiamata genera e restituisce un intero; chiamate ripetute generano una sequenza di interi che risultano uniformemente distribuiti nell'intervallo [O, RAND_MAX].



void srand(unsigned seed); Inizializza il generatore di numeri casuali, facendo in modo che la sequenza generata da ripetute chiamate a rand () inizi da una posizione differente. All'avvio del programma, il generatore di numeri casuali agisce come se fosse stata chiamata srand ( 1 ) . L'istruzione srand(time(NULL)); può essere utilizzata per inizializzare il generatore casuale con un valore differente a ogni esecuzione del programma.

Comunicazione con l'ambiente



char *getenv(const char *name); Effettua una ricerca in un elenco di variabili d'ambiente fornite dal sistema operativo. Se name è una delle variabili dell'elenco, viene restituito l'indirizzo base del valore corrispondente, altrimenti viene restituito NULL (vedi Paragrafo 11.12).



int system(const char *s); Passa la stringa s come comando da eseguire all'interprete di comandi (shell) fornito dal sistema operativo. Se s non è NULL, la funzione restituisce lo stato di ritorno del comando. Se s è NULL, la funzione restituisce un valore diverso da zero se l'interprete di comandi risulta disponibile mediante questo meccanismo, albimenti restituisce zero.

Aritmetica Intera



int abs(int i); long labs(long i); Entrambe le funzioni restituiscono il valore assoluto di i.

590

Appendice A



div_t div(int numer, int denom); ldiv_t ldiv(long numer, long denom); Entrambe le funzioni dividono numer per denom e restituiscono una struttura i cui membri sono il quoziente e il resto. Di seguito è riportato un esempio. div_t

d;

d= div(17, 5); printf("quotient =%d, remainder= %d\n•, d.quot, d.rem); In esecuzione, questo codice stampa la seguente riga: quotient = 3, remainder = 2 Conversione di stringhe

I membri delle due famiglie ato ... () e strto ... () vengono utilizzati per convertire una stringa in un valore. la conversione è concettuale; essa interpreta i caratteri nella stringa, ma la stringa stessa non viene modificata. la stringa può eventualmente iniziare con caratteri di spaziatura. La conversione termina al primo carattere inappropriato. Per esempio, la chiamata strtod(.123x4ss·, NULL) e la chiamata strtod(.\n 123 456•, NULL) restituiscono entrambe il valore double 123.0. La famiglia st rto ... ( ) fornisce maggior controllo sul processo di conversione e sugli errori. •

double atof(const char *s); /*da ascii a floating*/ Restituisce la stringa s convertita a double. Salvo che per il trattamento degli errori, la chiamata atof(s) è equivalente a: strtod(s, NULL)

Se non vengono effettuate conversioni la funzione restituisce zero. •

int atoi(const char *s), /* da ascii a intero */ Restituisce la stringa s convertita a in t. Salvo che per il trattamento degli errori, la chiamata atoi(s)

La lllnria ltandard 591

è equivalente a: (int) strtol(s, NULL, 10) Se non vengono effettuate conversioni la funzione restituiace zero. •

long atol(const char *s); /* da ascii a long •t Restituisce la stringa s convertita a long. Salvo che per il trattamento drrll t-rrori. la chiamata atol(s)

è equivalente a: strtol(s, NULL, 10) Se non vengono effettuate conversioni la funzione restituisce zero. •

double strtod(const char •s, char **end_ptr); Restituisce la stringa s convertita a double. Se non vengono effettuate conversioni viene restituito zero. Se end_ptr è diverso da NULLe viene effettuata la conversione, l'indirizzo del carattere che termina il processo di conversione viene posto nell'oggetto puntato da end_ptr. Se end_ptr è diverso da NULLe non viene effettuata la conversione, nell'oggetto puntato da end_pt r viene posto il valore s. In caso di overflow viene restituito HUGE_VAL oppure -HUGE_VAL, e in errno viene posto ERANGE; in caso di underflow viene restituito zero, e in errno viene posto ERANGE.



long strtol(const char *s, char ••end_ptr, int base); Restituisce la stringa s convertita a long. Se base ha un valore compreso tra 2 e 36, le cifre e le lettere in s sono interpretate in tale base. In base 36, le lettere da a a z e da A aZ vengono interpretate rispettivamente da 10 a 35. Con una base più piccola vengono interpretate solo le cifre e le lettere con valori corrispondenti minori della base. Se end_pt r è diverso da NULL e viene effettuata la conversione, nell'oggetto puntato da end_pt r viene posto l'indirizzo del carattere che termina il processo di conversione. Di seguito è riportato un esempio: char long

*p; value;

value = strtol("12345", &p, 3); printf("value = %ld, end string = \"%s\"\n", value, p); In esecuzione, questo codice stampa la seguente riga: value

= 5,

end string

=

"345•

592 Appendice A

Poiché la base è 3, il carattere 3 nella stringa " 1 2345 • interrompe il processo di conversione, e solo i primi due caratteri nella stringa vengono convertiti. In base 3, i caratteri 12 vengono convertiti al valore decimale 5. In modo analogo, il seguente codice value = strtol("abcde•, &p, 12); printf(•value = \ld, end string = \"\s\•\n•, value, p); stampa la riga: value

= 131,

end string

= "cde"

Poiché la base è 12, il carattere c nella stringa "abcde • interrompe il processo di conversione, e vengono convertiti solo i primi due caratteri nella stringa. In base 12, i caratteri ab vengono convertiti al valore decimale 131. Se base è zero, a seconda del primo carattere non di spaziatura in s la sbinga viene interpretata come intero esadecimale, ottale o decimale. Con un segno opzionale e 0X o 0x, la sbinga viene interpretata come intero esadecimale (base 16). Con un segno opzionale e 0, ma non 0X o 0x, la stringa viene interpretata come un intero ottale (base 8), altrimenti essa viene interpretata come intero decimale. Se non vengono effettuate conversioni viene restituito zero. Se end_ptr è diverso da NULLe non viene effettuata la conversione, il valore sviene posto nell'oggetto puntato da end_ptr. In caso di overflow viene restituito LONG_MAX o -LONG_MAX, e in errno viene posto ERANGE. •

unsigned long strtoul(const char *s, char **end_ptr, int base); Simile a strtol (),ma restituisce un unsigned long. In caso di overflow viene restituito ULONG_MAX o -ULONG_MAX.

Funzioni di caratteri multlbyte

I caratteri multi byte vengono utilizzati per rappresentare i membri di un insieme esteso di caratteri, la cui definizione dipende dal particolare sistema. •

int mblen(const char •s, size_t n); Se s è NULL, la funzione restituisce un valore diverso da zero o zero in funzione del fatto che i caratteri multi byte abbiano o meno una codifica dipendente dallo stato. Se s è diverso da NULL, la funzione esamina al più n caratteri in se restituisce il numero di byte che comprende il carattere multibyte successivo. Se s punta al carattere nullo viene restituito zero, se s non punta a un carattere multibyte viene restituito -1.



int mbtowc(wchar_t *p, const char •s, size_t n); Agisce come mblen (),con in più la seguente funzionalità: se p è diverso da NULL, la funzione converte il carattere multibyte successivo in s nel tipo di carattere esteso corrispondente e lo memorizza nell'oggetto puntato da p.

La libreria standard 593



int wctomb(char •s, wchar_t wc); Se s è NULL la funzione restituisce un valore diverso da zero o zero, in funzione del fatto che i caratteri multibyte abbiano o meno una codifica dipendente dallo stato. Se s è diverso da NULL e wc è un carattere esteso corrispondente a un carattere multibyte, la funzione memorizza il carattere multibyte in se restituisce il numero di byte necessari a rappresentarlo. Se s è diverso da NULL e wc non corrisponde a un carattere multibyte, viene restituito -l.

Funzioni di stringhe multlbyte • size_t mbstowcs(wchar_t •wcs, const char *mbs, size_t n); Legge la stringa multibyte puntata da mbs e scrive in wc s la corrispondente stringa di caratteri estesi. Vengono scritti al più n caratteri estesi seguiti da un carattere esteso nullo. Se la conversione ha successo viene restituito il numero di caratteri estesi scritti, senza contare il carattere nullo finale, altrimenti viene restituito -l. •

int wcstombs(char *mbs, const wchar_t •wcs, size_t n); Legge la stringa di caratteri estesi puntata da wc s e scrive la stringa di caratteri multibyte corrispondente in mb s. Il processo di conversione termina dopo che sono stati scritti n caratteri estesi o un carattere nullo. Se la conversione ha successo viene restituito il numero di caratteri scritto, senza contare l'eventuale carattere nullo; altrimenti viene restituito -l.

Uscita dal programmi • void abort(void); Provoca una terminazione anormale del programma, a meno che sia presente un gestore che intercetti il segnale SIGABRT e non rientri. La corretta chiusura dei file aperti e la rimozione dei file temporanei dipendono dall'implementazione. •

int atexit(void (* func)(void)); Fa sì che alla normale uscita dal programma venga eseguita la funzione puntata da fune. Se la chiamata ha successo viene restituito zero, altrimenti viene restituito un valore diverso da zero. È possibile registrare più funzioni (almeno 32) per l'esecuzione al termine del pr~ gramma: l'ordine di esecuzione è inverso rispetto a quello di registrazione. Tali funzioni possono utilizzare solo le variabili globali.



void exit(int status); Provoca la terminazione normale del programma. Le funzioni registrate da a t e xi t ( ) vengono chiamate in ordine inverso rispetto a quello secondo cui sono state registrate, gli stream bufferizzati vengono svuotati, i file vengono chiusi e l file temporanei creati con tmpf ile () vengono rimossi. D valore di statua, lneleme con il controllo, viene restituito all'ambiente chiamante. Se il valore di statue è zero o EXIT_SUCCESS, l'ambiente chiamante assume che l'esecuzione del pr~

594 Appendice A

gramma abbia avuto successo; se il valore è EXIT _FAI LURE viene assunto che l'esecuzione non abbia avuto successo. L'ambiente chiamante può riconoscere anche altri valori di status.

A.14 Gestione della memoria e delle stringhe: Questo file d'intestazione contiene i prototipi di funzioni di due famiglie. Le funzioni me m... ( ) vengono utilizzate per manipolare blocchi di memoria di dimensione specificata. Questi blocchi possono essere visti come array di byte (caratteri). Essi sono simili a stringhe, ma non vengono terminati dal carattere nullo. Le funzioni s t r ... ( ) vengono utilizzate per manipolare stringhe terminate dal carattere nullo. Di solito, all'inizio del file d'intestazione si trova la seguente riga: #include

l* per NULL e size_t */

Funzioni per la gestione della memoria



void •memchr(const void *p, int c, size_t n); Viene effettuata una ricerca, a partire dall'indirizzo di memoria p, del primo carattere senza segno (byte) il cui valore coincida con ( unsigned char) c. La ricerca viene effettuata al più su n byte; in caso di successo viene restituito un puntatore al carattere, altrimenti viene restituito NULL.



int memcmp(const void *p, const void *q, size_t n); Confronta due blocchi di memoria di lunghezza n. I byte vengono trattati come caratteri senza segno. La funzione restituisce un valore che è minore, uguale o maggiore di zero a seconda del fatto che il blocco puntato da p sia lessicograficamente minore, uguale o maggiore del blocco puntato da q.



void *memcpy(void *to, void *from, size_t n); Copia il blocco di n byte puntato da f rom nel blocco puntato da t o, restituendo il valore to. Se i blocchi si sovrappongono, il comportamento è indefinito.



void *memmove(void *to, void *from, size_t n); Copia il blocco di n byte puntato da f rom nel blocco puntato da t o, restituendo il valore di t o. Se i blocchi si sovrappongono, prima di scrivere un nuovo valore in un byte del blocco puntato da f rom, si accede a tale byte in modo che la copia venga effettuata correttamente anche in caso di sovrapposizione.



void •memset(void *p, int c, size_t n); Assegna a ogni byte nel blocco di lunghezza n puntato da p il valore ( unsigned char) c, restituendo p.

La libreria standard 595

Funzioni per la gestione di stringhe • char *5trcat(char *51, con5t char *52); Concatena le stringhe 51 e 52, aggiungendo una copia di 52 alla fine di 51. n pr~ grammatore deve assicurarsi che lo spazio a cui punta 51 sia sufficiente per contenere la stringa risultante. Viene restituita la stringa 51 .



char *5trchr(con5t char *5, int c); Cerca il primo carattere in s che coincida con il valore ( char) c, restituendone l'indirizzo. Se la ricerca non ha successo viene restituito NULL. La chiamata 5trchr ( 5, l \0 l) restituisce un puntatore al carattere nullo tenninatore di s.



int 5trcmp(const char *51, const char *s2); Confronta lessicograficamente le stringhe s 1 e 52, restituendo un valore minore, uguale o maggiore di zero a seconda che 51 sia rispettivamente minore, uguale o maggiore di 52. Gli elementi delle stringhe vengono trattati come caratteri privi di segno.



int 5trcoll(con5t char *51, const char *52); Confronta le due stringhe 51 e 52 utilizzando una regola di confronto che dipende dai parametri locali dell'implementazione e restituendo un valore minore, uguale o maggiore di zero a seconda che s 1 sia rispettivamente minore, uguale o maggiore di s2.



char •strcpy(char *51, const char *52); Copia la stringa s2 nella stringa s 1, compreso il carattere nullo tenninatore. 11 contenuto precedente di 51 viene perso. Il programmatore deve assicurarsi che lo spazio a cui punta s 1 sia sufficiente per contenere il risultato. Viene restituito 51.



size_t strc5pn(const char *s1, con5t char *s2); Calcola la lunghezza del massimo prefisso di s 1 formato solo da caratteri non presenti in s2. Per esempio, la chiamata di funzione strcspn("April i5 the cruele5t month", "abc") restituisce 13, in quanto Apri l i5 the " è il massimo prefisso del primo parametro che non ha caratteri in comune con "ab c" (il carattere c nel nome 5t rcspn sta per "complemento", e le lettere 5pn per "span"). N



char *strerror(int error_number); Restituisce un puntatore a una stringa d'errore fornita dal sistema, il cui contenuto non deve essere modificato dal programma. In caso d'errore il programmatore può utilizzare la chiamata strerror ( errno) per stampare il messaggio d'errore relativo al valore errno; a tale scopo è possibile utilizzare anche l'analoga funzione perror().

596

Appendice A



size_t strlen(const char *s); Restituisce la lunghezza della stringa s, cioè il numero di caratteri di s eccetto il carattere nullo tenninatore.



char *strncat(char *s1, const char *s2, size_t n); Aggiunge alla fine della stringa s 1 un massimo di n caratteri di s2 (senza contare il carattere nullo), seguiti dal carattere nullo. Il programmatore deve assicurarsi che lo spazio a cui punta s 1 sia sufficiente per contenere il risultato. Viene restituita la stringa s 1.



int strncmp(const char *s1, const char *s2, size_t n); Confronta lessicograficamente un massimo di n caratteri delle sbinghe s 1 e s2. Il confronto termina quando viene raggiunto l'n-esimo carattere o un carattere nullo terminatore, restituendo un valore minore, uguale o maggiore di zero a seconda che la porzione di s 1 considerata sia rispettivamente minore, uguale o maggiore della porzione considerata di s2. Gli elementi delle stringhe vengono trattati come caratteri privi di segno.



char *strncpy(char *s1, const char *s2, size_t n); Vengono scritti in s1 esattamente n caratteri, perdendo il contenuto precedente. Gli n caratteri corrispondono ai primi n caratteri di s2; se la lunghezza di s2 è minore di n, ai caratteri restanti viene assegnato il valore \0 Se la lunghezza di s 1 è n e quella di s2 è maggiore o uguale a n, allora s 1 non avrà il carattere nullo tenninatore. Il programmatore deve assicurarsi che lo spazio a cui punta s 1 sia sufficiente per memorizzare il risultato. Viene restituito il valore s 1. l



l •

char *strpbrk(const char *s1, const char *s2); Cerca in s1 il primo carattere coincidente con uno dei caratteri di s2, restituendone l'indirizzo o, in caso d'insuccesso, NULL. Per esempio, la chiamata di funzione strpbrk (·Apri l is the cruelest month", "abc •) restituisce l'indirizzo della c in cruelest. Le lettere pbrk nel nome st rpbrk sono un'abbreviazione di "pointer to break".



char •strrchr(const char •s, int c); Cerca, partendo dalla fine della stringa, il primo carattere in s che coincida con il valore ( char) c, restituendone l'indirizzo o, se tale carattere non viene trovato, NULL. La chiamata strchr( s, \0 restituisce un puntatore al carattere nullo tenninatore di s. l



l

)

size_t strspn(const char *s1, const char *s2); Calcola la lunghezza del massimo prefisso di s 1 formato solo da caratteri presenti in s2.

La libreria standard 597

Per esempio, la chiamata di funzione strspn("April is the cruele5t month", "A 15 for apple") restituisce 9, in quanto tutti i caratteri del primo parametro che precedono la t in the sono presenti nel secondo parametro, mentre t non lo è. Le lettere spn nel nome 5t r5pn stanno per "span". •

char *strstr(con5t char *s1, const char *s2); Cerca in 51 la prima occorrenza della sottostringa s2; se tale occorrenza viene trovata viene restituito un puntatore all'indirizzo base della sottostringa in s 1, altrimenti viene restituito NULL.



char *strtok(char *s1, const char *52); Effettua una ricerca di token in s 1 utilizzando i caratteri in s2 come separatori di token. Se 51 contiene uno o più token viene individuato il primo di essi, il carattere che lo segue immediatamente viene sostituito da un carattere nullo, il resto della stringa s 1 viene posto dal sistema in un'altra area e viene restituito l'indirizzo del primo carattere del token. Chiamate seguenti con s1 uguale a NULL restituiscono l'indirizzo base di una stringa fornita dal sistema che contiene il token successivo. Se non risultano disponibili altri token viene restituito NULL. La chiamata iniziale st rtok ( s 1 , s2) restituisce NULL se s 1 non contiene token. Di seguito è riportato un esempio: char char char

s1[] 52[ 1 *p;

• thi5 is,an •

,, , •

example ; ";

Il •

printf(" \ "\5\"", 5trtok(51, 52)); while ((p = 5trtok(NULL, s2)) l= NULL) printf (" \ "%5 \"", p) ; putchar( l \n l); In esecuzione, tale codice stampa la seguente riga: "thi5• •15" "an" "example• •

5ize_t strxfrm(char *51, con5t char *52, 5ize_t n); Trasforma la stringa 52 ponendo il risultato in 51 , cancellando quindi il contenuto precedente di quest'ultima. In 51 vengono scritti un massimo di n caratteri, tra cui il carattere nullo finale; viene restituita la lunghezza di 51. La trasformazione è tale che quando due stringhe trasformate vengono utilizzate come parametri di 5t re mp ( ) il valore restituito è minore, uguale o maggiore di zero a seconda che 5t reo l l () applicata alle stringhe non trasformate restituisca un valore rispettivamente minore, uguale o maggiore di zero; le lettere xfrm nel nome strxfrm stanno per "transform".

598 Appendice A

A.15 Data e ora: In questo file d'intestazione sono contenuti i prototipi delle funzioni che operano sulla data, sull'ora e sull'orologio interno. Di seguito sono riportati alcuni esempi di macro e definizioni di tipo.

l* per NULL e size_t *l

#include #define

CLOCKS_PER_SEC

typedef typedef

long long

60

l* dipendente dalla macchina *l

clock t; t ime_t;

Gli oggetti di tipo struct tm vengono utilizzati per memorizzare la data e l'ora. struct t m{ in t tm_sec; tm_min; in t in t tm hour; in t tm:=mday; in t tm_mon; tm_year; in t in t tm_wday; tm_yday; in t in t tm_isdst; };

l* l* l* l* l* l* l* l* l*

secondi: [0, 60] *l minuti: [0, 59] *l ore: [0, 23] *l giorno del mese: [1, 31] *l mesi da Gennaio: [0, 11] *l anno dal 1900 *l giorni da Domenica: [0, 6] *l giorni dal primo Gennaio: [0, 365] *l flag per l'ora legale *l

Si osservi che l'intervallo di valori per tm_sec permette un ''secondo bisestile" che si presenta solo sporadicamente. Nel flag tm_isdst è contenuto un valore positivo se è in vigore l'ora legale, zero in caso contrario, negativo se l'informazione non risulta disponibile. Accesso all'orologio Interno Su molti sistemi la funzione clock () fornisce l'accesso al sottostante orologio della macchina. La frequenza dell'orologio (ticchettii per secondo) dipende dalla macchina. •

clock_t clock(void); Restituisce un'approssimazione del numero di "ticchettii dell'orologio" della CPU utilizzati dal programma fino al punto della chiamata. Il valore restituito può essere diviso per CLOCKS_PER_SEC per essere convertito in secondi. Se l'orologio della CPU non è disponibile viene restituito il valore -l. Per un'ulteriore analisi consulti il Paragrafo 11.16.

Accesso al tempo In ANSI C il tempo viene trattato principalmente in due modi: in forma compatta (tempo del calendario}, espressa mediante un intero che su molti sistemi rappresenta il numero

La libreria stancMrd •

di secondi trascorsi dal l o gennaio 1970, e in forma suddivisa, espressa mediante una struttura di tipo st ruct t m. D tempo del calendario è codificato rispetto alle Coordinate Universali di Tempo (UTC). n programmatore può utilizzare funzioni di libreria per convertire una versione del tempo in un'altra, e sono inoltre disponibili delle funzioni per stampare il tempo come stringa. •

time_t time(time_t •tp); Restituisce il tempo corrente in forma compatta, espresso come numero di secondi trascorsi dal l o gennaio 1970 (UTC). È possibile, anche se raro, che su alcuni sistemi vengano utilizzate altre unità e altre date di partenza. Se t p è diverso da NULL, il valore ottenuto viene anche memorizzato nell'oggetto puntato da tp. Si consideri il seguente codice: time_t

now;

now = time(NULL); printf("\n\s\ld\n\s\s\s\s\n", " now ctime(&now) = " "asctime(localtime(&now)) = •

now, et ime ( &now), asctime(localtime(&now)));

Eseguito sul nostro sistema, tale codice ha prodotto il seguente output now = 685136007 ctime(&now) = Tue Sep 17 12:33:27 1991 asctime(localtime(&now)) =Tue Sep 17 12:33:27 1991 •

char •asctime(const struct tm *tp); Converte il tempo rappresentato in forma suddivisa, presente nell'oggetto puntato da tp, in una stringa fornita dal sistema. La funzione restituisce l'indirizzo base della stringa. Chiamate successive ad asctime () e et ime () riscrivono la stringa.



char •ctime(const time_t *t_ptr); Converte il tempo espresso in forma compatta, presente nell'oggetto puntato da t_ptr, in una stringa fornita dal sistema. La funzione restituisce l'indirizzo di base della stringa. Chiamate successive ad asctime () e et ime () riscrivono la stringa. Le chiamate di funzione ctime(&now)

e

asctime(localtime(&now))

sono equivalenti. •

double difftime(time_t t0, time_t t1); Calcola la differenza t1 - t0 e, se necessario, converte questo valore nel numero di secondi che sono passati tra i tempi t0 e t1 espressi in forma compatta. D risultato viene restituito come double.

600 Appendice A



struct tm •gmtime(const time_t •t_ptr); Converte il tempo espresso in forma compatta, presente nell'oggetto puntato da t_ptr, nella corrispondente forma suddivisa, memorizzando il risultato in un oggetto di tipo st ruct t mfornito dal sistema. Viene restituito l'indirizzo della struttura. La funzione calcola la forma suddivisa rispetto alle Coordinate Universali di Tempo (UTC), in passato chiamate Greenwich Mean lime (cioè GMT, da cui il nome della funzione). Chiamate successive a gmt ime ( ) e local t ime ( ) riscrivono la struttura.



struct tm *localtime(const time_t *t_ptr); Converte il tempo espresso in forma compatta, presente nell'oggetto puntato da t_ptr, nella corrispondente forma suddivisa locale, memorizzando il risultato in un oggetto di tipo st ruct t mfornito dal sistema. Viene restituito l'indirizzo della struttura. Chiamate successive a gmt ime ( ) e local t ime ( ) rise rivo no la struttura.



time_t mktime(struct tm *tp); Converte il tempo locale scritto in forma suddivisa, presente nella struttura puntata da tp, nella corrispondente forma compatta, che viene restituita in caso di successo. In caso di insuccesso viene invece restituito -l. Durante il calcolo, i membri tm_wday e tm_yday vengono ignorati. Prima del calcolo altri membri possono avere valori al di fuori dell'intervallo usuale, e dopo il calcolo i membri della struttura possono essere riscritti con un insieme di valori equivalenti nel quale ogni membro si trova nell'intervallo corretto. I valori di tm_wday e tm_yday vengono ricavati da quelli degli altri membri. Per esempio, il seguente codice può essere utilizzato per determinare la data del giorno fra 1.000 giorni da oggi:

struct tm time_t

*tp; now, later;

now = time(NULL); tp = localtime(&now); tp -> tm_mday += 1000; later = mktime(tp); printf("\n1000 days from now: %s\n", ctime(&later)); •

size_t strftime(char •s, size_t n, const char •cntrl_str, const struct tm *tp); Scrive i caratteri nella stringa puntata da s sotto la direzione della stringa di controllo puntata da cntrl_str. Vengono scritti un massimo di n caratteri, tra cui il carattere nullo. Se sono richiesti più di n caratteri la funzione restituisce zero e il contenuto di s è indeterminato, altrimenti viene restituita la lunghezza di s. La stringa di controllo è formata da caratteri normali e da specifiche di conversione, o formati, che determinano il modo in cui vengono scritti i valori del tempo rappresentato nella struttura puntata da tp. Ogni specifica di conversione è formata dal carattere % seguito da un carattere di conversione.

La libreria standard 601

strftime () Specifica di conversione

Che cosa viene stampato

Esempio

%a

giorno della settimana abbreviato giorno della settimana per esteso mese abbreviato mese per esteso data e ora giorno del mese ora, espressa da Oa 23 ora, espressa da Oa 11 giorno dell'anno mese dell'anno minuti AMoPM secondi settimana dell'anno (da Doma Sab) giorno della settimana (da O a 6) data ora anno del secolo anno fuso orario simbolo di percentuale

Fri Friday Se p September Sep 01 02:17:23 1993 01 02 02 243 9 17

%A

%b %8 %c %d %H %h %j %m

%M %p %s %U %w %x %X %y %Y %Z %%

AM

23 34 5 Sep 01 1993 02:17:23 93 1993 PDT %

Si consideri il seguente codice: char time_t

s [ 100]; now;

now = time(NULL); strftime(s, 100, "%H:%M:%S on %A, %d %8 %Y'', localtime(&now)); printf( •%s\n\n", s); Eseguendo un programma contenente queste righe è stato ottenuto l'output: 13:01:15 on Tuesday, 17 September 1991

A.16 Altre funzioni Oltre alle funzioni specificate da ANSI C, è possibile che il sistema fornisca altre funzi compound_statement function_name ::- identijier parameter_declaration_list ::- parameter_declaration l , parameter_declaration lo.

606 Appendice B

8.3

Dichiarazioni

declaration ::= declaration_specifiers init_declarator_listopt declaration_specifiers ::= storage_class_specifier_or_typedef declaration_specifiersopt l type_specifier declaration_specijiersopt l type_qualifier declaration_specifiersopt storage_class_specifier_or_typedef ::= auto l extern l register l sta t i c l typedef

type_specifier ::= char l double l float l int l long l short l signed l unsigned l voi d l enum_specifier l struct_or_union_specifier l typedef_name enum_specifier ::= enum tagopt l enumerator_list t l enum tag tag ::= identifier enumerator_list ::= enumerator {, enumerator }opt enumerator ::= enumeration_constant { = const_tntegral_expr }opt

enumeration_constant ::- identifier struct_or_union_specijier ::· struct_or_union tagopt l struct_declaration_list t l struct_or_union tag struct_or_union ::s struct l union struct_declaration_list ::= { struct_declaration }1• struct_declaration ::= type_specifier_qualifier_list struct_declarator_list ; type_specifier_qualifier_list ::= type_specifier type_specijier_qualifier_listopt l type_qualifier type_specifier_qualifier_listopt struct_declarator_list ::= struct_declarator { , struct_declarator }O+ struct_declarator ::= declarator l declaratoropt: const_integral_expr type_qualifier ::= const l volatile declarator ::= pointeropt direct_declarator pointer ::"" { * l type_qualifier_listopt }l+ type_qualifier_list ::= { type_qualifier }1• direct_declarator ::= identijier l (declarator) l direct_declarator [const_integral_expropt] l direct_declarator (parameter_type_list) l direct_declarator (identifier_listopt) parameter_type_list ::"" parameter_list l parameter_list , ... parameter_list ::= parameter_declaration { , parameter_declaration }o. parameter_declaration ::= declaration_specifiers declarator l declaration_specifiers abstract_declaratoropt abstract_declarator ::= pointer l pointeropt direct_abstract_declarator dìrect_abstract_declarator ::= (abstract_declarator) l direct_abstract_declaratoropt [const_integral_expropt]

Sintassi del linguaggio C 607

l direct_abstract_declaratoropt (parameter_type_listopt) identijier_list ::- identifier ( , identifier lo. typede/_name ::· identijier init_declarator_list ::- init_declarator { ' init_declarator ,opt init_declarator ::• declarator l declarator = initializer initializer ::- assignment_expression l ( initializer_list l l ( initializer_list , l initializer_list ::- initializer { , initializer lo.

8.4

Istruzioni

statement ::- compound_statement l expression_statement l iteration_statement l jump _statement l labeled_statement l selection_statement compound_statement ::= ( declaration_listop, statement_listopt l declaration_list ::= ( declaration }1• statement_list ::- ( statement }1• expression_statement ::= expressionopt ; jump_statement ::·break ; l continue ; l goto identijier ; l return expressionopt ; labeled_statement ::= identijier : statement l case const_integral_expression : statement l default : statement selection_statement ::= i f (expression) statement l i f (expression) statement else statement l switch_statement switch_statement ::- swi tch (integral_expression) ( case_statement l default : statement l switch_block 11 case_stateme11t ::- { case const_integral_expression: J1• statement switch_block ::• { { declaration_list }opt case_default_group } case_de/ault_group ::· ( case_group }t+ l ( case_group }+ default_group ( case_group }+ case_group ::- { case const_integral_expression : J1• ( statement J 1• default_group ::-default : ( statement }1•

8.5

Espressioni

expression ::- constant l string_literal l (expression) l lvalue l assignment_expression l expression , expression l + expression l - expression l function_expression l relational_expression l equality_expression l logical_expression

608 Appendice B

expression arithmetic_op expression l bitwise_expression expression? expression : expression l sizeof expression sizeof (type_name) l (type_name) expression lvalue ::= & lvalue l ++ lvalue l lvalue ++ l -- lvalue l lvalue -1 identijier l * expression l lvalue [ expression ] l (lvalue) l lvalue . identijier l lvalue -> identijier assignment_expression ::= lvalue assignment_op expression assignment_op ::= = l += l -= l *= l /= l %= l &= l "= l l= l >>= l H R \ f p

s

z